Skip to content
71 changes: 71 additions & 0 deletions pkg/lint/rules.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,28 @@ import (
)

var (
daemonFlags = []string{
`(?:^|\s)--daemon\b`,
`(?:^|\s)--daemonize\b`,
`(?:^|\s)--detach\b`,
`(?:^|\s)-daemon\b`,
}

redirPatterns = []string{
`>\s*\S+`,
`>>\s*\S+`,
`2>\s*\S+`,
`2>>\s*\S+`,
`&>\s*\S+`,
`&>>\s*\S+`,
`>\s*\S+.*2>&1`,
`2>&1.*>\s*\S+`,
`>\s*/dev/null`,
`2>\s*/dev/null`,
`&>\s*/dev/null`,
`\d+>&\d+`,
}

reValidSHA256 = regexp.MustCompile(`^[a-fA-F0-9]{64}$`)
reValidSHA512 = regexp.MustCompile(`^[a-fA-F0-9]{128}$`)
reValidSHA1 = regexp.MustCompile(`^[a-fA-F0-9]{40}$`)
Expand All @@ -43,6 +65,14 @@ var (
hostEditDistanceExceptions = map[string]string{
"www.libssh.org": "www.libssh2.org",
}

// Detect background processes (commands ending with '&' or '& sleep ...') or daemonized commands
// reBackgroundProcess detects background processes (commands ending with '&' or '& sleep ...')
// We explicitly avoid matching '&&' which is commonly used for command chaining.
reBackgroundProcess = regexp.MustCompile(`(?:^|[^&])&(?:\s*$|\s+sleep\b)`) // matches 'cmd &' or 'cmd & sleep'
reDaemonProcess = regexp.MustCompile(`.*(?:` + strings.Join(daemonFlags, "|") + `).*`)
// Detect output redirection in shell commands
reOutputRedirect = regexp.MustCompile(strings.Join(redirPatterns, "|"))
)

const gitCheckout = "git-checkout"
Expand Down Expand Up @@ -456,6 +486,47 @@ var AllRules = func(l *Linter) Rules { //nolint:gocyclo
return fmt.Errorf("auto-update is disabled but no reason is provided")
},
},
{
Name: "background-process-without-redirect",
Description: "test steps should redirect output when running background processes",
Severity: SeverityWarning,
LintFunc: func(c config.Configuration) error {
checkSteps := func(steps []config.Pipeline) error {
for _, s := range steps {
if s.Runs == "" {
continue
}
lines := strings.Split(s.Runs, "\n")
for i, line := range lines {
checkLine := line
if strings.Contains(line, "&") && i+1 < len(lines) {
checkLine += "\n" + lines[i+1]
}

needsRedirect := reBackgroundProcess.MatchString(checkLine) || reDaemonProcess.MatchString(line)
if needsRedirect && !reOutputRedirect.MatchString(line) {
return fmt.Errorf("background process missing output redirect: %s", strings.TrimSpace(line))
}
}
}
return nil
}

if c.Test != nil {
if err := checkSteps(c.Test.Pipeline); err != nil {
return err
}
}
for _, sp := range c.Subpackages {
if sp.Test != nil {
if err := checkSteps(sp.Test.Pipeline); err != nil {
return err
}
}
}
return nil
},
},
{
Name: "valid-update-schedule",
Description: "update schedule config should contain a valid period",
Expand Down
89 changes: 89 additions & 0 deletions pkg/lint/rules_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -469,6 +469,95 @@ func TestLinter_Rules(t *testing.T) {
wantErr: true,
matches: 1,
},
{
file: "background-process-no-redirect.yaml",
minSeverity: SeverityWarning,
want: EvalResult{
File: "background-process-no-redirect",
Errors: EvalRuleErrors{
{
Rule: Rule{
Name: "background-process-without-redirect",
Severity: SeverityWarning,
},
Error: fmt.Errorf("[background-process-without-redirect]: background process missing output redirect: croc relay --ports=1234 & (WARNING)"),
},
},
},
wantErr: false,
matches: 1,
},
{
file: "background-process-multiline-no-redirect.yaml",
minSeverity: SeverityWarning,
want: EvalResult{
File: "background-process-multiline-no-redirect",
Errors: EvalRuleErrors{
{
Rule: Rule{
Name: "background-process-without-redirect",
Severity: SeverityWarning,
},
Error: fmt.Errorf("[background-process-without-redirect]: background process missing output redirect: coredns & (WARNING)"),
},
},
},
wantErr: false,
matches: 1,
},
{
file: "background-process-with-redirect.yaml",
minSeverity: SeverityWarning,
want: EvalResult{},
wantErr: false,
matches: 0,
},
{
file: "double-ampersand-valid.yaml",
minSeverity: SeverityWarning,
want: EvalResult{},
wantErr: false,
matches: 0,
},
{
file: "daemon-flag-no-redirect.yaml",
minSeverity: SeverityWarning,
want: EvalResult{
File: "daemon-flag-no-redirect",
Errors: EvalRuleErrors{
{
Rule: Rule{
Name: "background-process-without-redirect",
Severity: SeverityWarning,
},
Error: fmt.Errorf("[background-process-without-redirect]: background process missing output redirect: croc relay --daemon (WARNING)"),
},
},
},
wantErr: false,
matches: 1,
},
{
file: "daemon-flag-with-redirect.yaml",
minSeverity: SeverityWarning,
want: EvalResult{},
wantErr: false,
matches: 0,
},
{
file: "avahi-no-daemon.yaml",
minSeverity: SeverityWarning,
want: EvalResult{},
wantErr: false,
matches: 0,
},
{
file: "cut-d-flag.yaml",
minSeverity: SeverityWarning,
want: EvalResult{},
wantErr: false,
matches: 0,
},
}

for _, tt := range tests {
Expand Down
45 changes: 45 additions & 0 deletions pkg/lint/testdata/files/avahi-no-daemon.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package:
name: avahi-no-daemon
version: 1.0.0
epoch: 0
description: Package running avahi commands without backgrounding
copyright:
- paths:
- "*"
attestation: TODO
license: GPL-2.0-only
pipeline:
- uses: fetch
with:
uri: https://test.com/avahi/${{package.version}}.tar.gz
expected-sha256: ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269
test:
pipeline:
# AUTOGENERATED
- runs: |
avahi-browse --version
avahi-browse-domains --version
avahi-publish --version
avahi-publish-address --version
avahi-publish-service --version
avahi-resolve --version
avahi-resolve-address --version
avahi-resolve-host-name --version
avahi-set-host-name --version
avahi-autoipd --version
avahi-daemon --version
avahi-dnsconfd --version
avahi-browse --help
avahi-browse-domains --help
avahi-publish --help
avahi-publish-address --help
avahi-publish-service --help
avahi-resolve --help
avahi-resolve-address --help
avahi-resolve-host-name --help
avahi-set-host-name --help
avahi-autoipd --help
avahi-daemon --help
avahi-dnsconfd --help
update:
enabled: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package:
name: background-process-multiline-no-redirect
version: 1.0.0
epoch: 0
description: Package with multiline background process without redirect
copyright:
- paths:
- "*"
attestation: TODO
license: GPL-2.0-only
pipeline:
- uses: fetch
with:
uri: https://test.com/background/${{package.version}}.tar.gz
expected-sha256: ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269
test:
pipeline:
- runs: |
cat > Corefile <<EOF
.:1053 {
file /home/build/db.wolfi.dev
log
errors
cache
}
EOF

cat > /home/build/db.wolfi.dev <<'EOF'
$TTL 3600
@ IN SOA ns1.wolfi.dev. admin.wolfi.dev. (
20240101 ; Serial
7200 ; Refresh
3600 ; Retry
1209600 ; Expire
3600 ) ; Negative Cache TTL
;
@ IN NS ns1.wolfi.dev.
;
foo.wolfi.dev IN TXT "hi"
EOF

coredns &
sleep 2
update:
enabled: true
20 changes: 20 additions & 0 deletions pkg/lint/testdata/files/background-process-no-redirect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package:
name: background-process-no-redirect
version: 1.0.0
epoch: 0
description: Package with background process without redirect
copyright:
- paths:
- "*"
attestation: TODO
license: GPL-2.0-only
pipeline:
- uses: fetch
with:
uri: https://test.com/background/${{package.version}}.tar.gz
expected-sha256: ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269
test:
pipeline:
- runs: "croc relay --ports=1234 &"
update:
enabled: true
20 changes: 20 additions & 0 deletions pkg/lint/testdata/files/background-process-with-redirect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package:
name: background-process-with-redirect
version: 1.0.0
epoch: 0
description: Package with background process with redirect
copyright:
- paths:
- "*"
attestation: TODO
license: GPL-2.0-only
pipeline:
- uses: fetch
with:
uri: https://test.com/background/${{package.version}}.tar.gz
expected-sha256: ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269
test:
pipeline:
- runs: "croc relay --ports=1234 > croc.log 2>&1 &"
update:
enabled: true
20 changes: 20 additions & 0 deletions pkg/lint/testdata/files/cut-d-flag.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package:
name: cut-d-flag
version: 1.0.0
epoch: 0
description: Package using cut -d but not running daemon
copyright:
- paths:
- "*"
attestation: TODO
license: GPL-2.0-only
pipeline:
- uses: fetch
with:
uri: https://test.com/cut/${{package.version}}.tar.gz
expected-sha256: ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269
test:
pipeline:
- runs: "getcap /usr/bin/fping | cut -d ' ' -f2 | grep -q -E '^cap_net_raw=+ep$'"
update:
enabled: true
20 changes: 20 additions & 0 deletions pkg/lint/testdata/files/daemon-flag-no-redirect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package:
name: daemon-flag-no-redirect
version: 1.0.0
epoch: 0
description: Package with daemon flag without redirect
copyright:
- paths:
- "*"
attestation: TODO
license: GPL-2.0-only
pipeline:
- uses: fetch
with:
uri: https://test.com/daemon/${{package.version}}.tar.gz
expected-sha256: ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269
test:
pipeline:
- runs: "croc relay --daemon"
update:
enabled: true
20 changes: 20 additions & 0 deletions pkg/lint/testdata/files/daemon-flag-with-redirect.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package:
name: daemon-flag-with-redirect
version: 1.0.0
epoch: 0
description: Package with daemon flag and redirect
copyright:
- paths:
- "*"
attestation: TODO
license: GPL-2.0-only
pipeline:
- uses: fetch
with:
uri: https://test.com/daemon/${{package.version}}.tar.gz
expected-sha256: ab5a03176ee106d3f0fa90e381da478ddae405918153cca248e682cd0c4a2269
test:
pipeline:
- runs: "croc relay --daemon > croc.log 2>&1"
update:
enabled: true
Loading