diff --git a/docs/README.md b/docs/README.md index 1b8243f..d4815d9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -108,6 +108,48 @@ never fail the run. The default `--fail-on=any` fails on any finding, as before. `--strict` is for gates that must not silently pass because a scanner was missing on the machine. +### What the scanners cover + +| Surface | Scanner | +|---|---| +| npm / yarn / pnpm / bun lockfiles | osv | +| Maven / Gradle (`pom.xml`, `gradle.lockfile`) | osv | +| Python / Ruby / Go / Rust / PHP / Dart / .NET lockfiles | osv | +| Secrets in git history | gitleaks | +| Secrets in working files | trufflehog | +| JS/TS source (SAST) | semgrep `p/default` | +| Swift / Kotlin / Java mobile source | semgrep `p/mobsfscan` (auto, on native mobile source) | +| Bicep, Terraform, GitHub Actions, Docker, K8s | checkov | +| Malicious package behaviour | socket (opt-in) | + +Not covered - know your blind spots: CocoaPods advisories (OSV has no +CocoaPods ecosystem, so `Podfile.lock` yields nothing), compiled IPA/APK/AAB +binaries, Expo `app.json`/`eas.json` and Info.plist/AndroidManifest +semantics, and a bare `package.json` with no lockfile. Expo-managed repos +with no `ios/`/`android/` dir are fine: their native layer is the npm +packages, which are covered. Each repo's report includes a `Coverage:` line +listing exactly which manifest files osv parsed (or "no package manifests +parsed", so silence never masquerades as coverage). + +### Baselines (accepted findings) + +Accepted findings live in each tool's native file at the repo root - no +SecKit-specific format, so reasons and expiry are reviewed in the PR that +edits the file: `.gitleaks.toml` / `.gitleaksignore`, `osv-scanner.toml` +(`[[IgnoredVulns]]` with `reason` and `ignoreUntil`), `.semgrepignore` / +`# nosemgrep`, `.checkov.baseline` (create with `checkov -d . +--create-baseline`), `.trufflehog-exclude` (one path regex per line). The +checkov and trufflehog files are passed automatically when present; +gitleaks reads its ignore files from the scanned repo. + +Two placement/scope caveats, both verified against the shipped tools: +`osv-scanner.toml` must sit **beside each manifest** it applies to (osv only +loads the config next to the lockfile being scanned - a root-level file does +nothing for a nested `apps/*/package-lock.json`). And checkov baselines match +on **resource name + check id only, across files**: a same-named resource in +a new file silently inherits the suppression, so review `.checkov.baseline` +diffs with care. + ## Harden a repo against AI agents `seckit harden` stops **Claude Code and GitHub Copilot** pulling secrets into diff --git a/scan_repos.ps1 b/scan_repos.ps1 index 10d2008..ad0deac 100644 --- a/scan_repos.ps1 +++ b/scan_repos.ps1 @@ -172,6 +172,17 @@ function Test-CleanWarning([string]$Key, [string]$Log) { return $false } +# osv logs "Scanned file and found N packages" - lift those lines into +# a Coverage line so the scan says what was actually parsed. Paths relative +# to the repo. +function Get-Coverage([string]$Repo, [string]$Log) { + if (-not (Test-Path -LiteralPath $Log)) { return '' } + $text = Get-Content -LiteralPath $Log -Raw -ErrorAction SilentlyContinue + $found = [regex]::Matches("$text", 'Scanned [^\r\n]+ file and found [0-9]+ packages?') | + ForEach-Object { $_.Value.Replace("Scanned $Repo/", 'Scanned ').Replace("Scanned $Repo\", 'Scanned ') } + return (@($found) -join '; ') +} + # -FailOn high: is this non-zero scanner exit below the failure threshold? # Mirrors seckit_below_threshold in scan_repos.sh. function Test-BelowThreshold([string]$Key, [string]$Log) { @@ -224,20 +235,36 @@ foreach ($repo in $repos) { if ((Want 'osv') -and (Have osv-scanner)) { Write-Host "- osv-scanner (vulnerable deps)" -ForegroundColor DarkGray $rc = Invoke-Scan -Key 'osv' -Repo $repo -Cmd { & osv-scanner -r $repo } + $cov = Get-Coverage -Repo $repo -Log $Results[$Results.Count - 1].Log + if (-not $cov) { $cov = 'no package manifests parsed' } + Write-Host " coverage: $cov" -ForegroundColor DarkGray if ($rc -ne 0) { $hit = $true } } if ((Want 'gitleaks') -and $gl) { Write-Host "- gitleaks (secrets in git history)" -ForegroundColor DarkGray if ($gl -eq 'git') { - $rc = Invoke-Scan -Key 'gitleaks' -Repo $repo -Cmd { & gitleaks git $repo --redact --no-banner } + # --gitleaks-ignore-path: read .gitleaksignore from the scanned repo, + # not from whatever directory seckit happens to be invoked from. + $rc = Invoke-Scan -Key 'gitleaks' -Repo $repo -Cmd { & gitleaks git $repo --redact --no-banner --gitleaks-ignore-path $repo } } else { - $rc = Invoke-Scan -Key 'gitleaks' -Repo $repo -Cmd { & gitleaks detect --source $repo --redact --no-banner } + $rc = Invoke-Scan -Key 'gitleaks' -Repo $repo -Cmd { & gitleaks detect --source $repo --redact --no-banner --gitleaks-ignore-path $repo } } if ($rc -ne 0) { $hit = $true } } if ((Want 'trufflehog') -and (Have trufflehog)) { Write-Host "- trufflehog (secrets in files)" -ForegroundColor DarkGray - $rc = Invoke-Scan -Key 'trufflehog' -Repo $repo -Cmd { & trufflehog filesystem $repo --no-update --fail 2> $null } + $thArgs = @() + if (Test-Path -LiteralPath (Join-Path $repo '.trufflehog-exclude')) { $thArgs = @('-x', (Join-Path $repo '.trufflehog-exclude')) } + $rc = Invoke-Scan -Key 'trufflehog' -Repo $repo -Cmd { + $thErr = Join-Path $LogDir 'trufflehog.stderr' + & trufflehog filesystem $repo --no-update --fail @thArgs 2> $thErr + # --fail exits 183 on findings; any other non-zero is a tool error + # (e.g. a bad regex in .trufflehog-exclude) - surface the reason. + if ($LASTEXITCODE -ne 0 -and $LASTEXITCODE -ne 183) { + "trufflehog error (exit $LASTEXITCODE):" + Get-Content -LiteralPath $thErr -Tail 3 -ErrorAction SilentlyContinue + } + } if ($rc -ne 0) { $hit = $true } } if ((Want 'semgrep') -and (Have semgrep)) { @@ -256,7 +283,11 @@ foreach ($repo in $repos) { } if ((Want 'checkov') -and (Have checkov)) { Write-Host "- checkov (IaC misconfig)" -ForegroundColor DarkGray - $rc = Invoke-Scan -Key 'checkov' -Repo $repo -Cmd { & checkov -d $repo --quiet --compact --skip-path node_modules } + # Accepted findings live in checkov's own baseline format; create with + # `checkov -d . --create-baseline`. Passed only when the file exists. + $ckArgs = @('-d', $repo, '--quiet', '--compact', '--skip-path', 'node_modules') + if (Test-Path -LiteralPath (Join-Path $repo '.checkov.baseline')) { $ckArgs += @('--baseline', (Join-Path $repo '.checkov.baseline')) } + $rc = Invoke-Scan -Key 'checkov' -Repo $repo -Cmd { & checkov @ckArgs } if ($rc -ne 0) { $hit = $true } } if ((Want 'socket') -and (Have socket)) { @@ -398,12 +429,16 @@ if ($ranKeys.Count -and $repos.Count) { [void]$md.AppendLine('_`-` = scanner did not apply to that repo, `0` = clean, counts are approximate._') [void]$md.AppendLine() foreach ($row in ($Results | Where-Object { $_.Scanner -eq 'osv' })) { + $rel = if ($row.Repo -eq $Root) { '.' } else { $row.Repo.Substring($Root.Length).TrimStart('\','/') } $sev = Get-OsvSeverity -Log $row.Log if ($sev) { - $rel = if ($row.Repo -eq $Root) { '.' } else { $row.Repo.Substring($Root.Length).TrimStart('\','/') } [void]$md.AppendLine("**osv severity (``$rel``):** $sev") [void]$md.AppendLine() } + $cov = Get-Coverage -Repo $row.Repo -Log $row.Log + if (-not $cov) { $cov = 'no package manifests parsed' } + [void]$md.AppendLine("**Coverage (``$rel``):** $cov") + [void]$md.AppendLine() } } diff --git a/scan_repos.sh b/scan_repos.sh index 85fc2ac..aaae03e 100644 --- a/scan_repos.sh +++ b/scan_repos.sh @@ -198,7 +198,29 @@ run_scan() { # trufflehog: keep findings (stdout) but drop noisy progress (stderr). _seckit_trufflehog() { - trufflehog filesystem "$1" --no-update --fail 2>/dev/null + local x=() err rc + [[ -f "$1/.trufflehog-exclude" ]] && x=(-x "$1/.trufflehog-exclude") + err="${TMPDIR:-/tmp}/seckit-th-$$.err" + trufflehog filesystem "$1" --no-update --fail ${x[@]+"${x[@]}"} 2>"$err" + rc=$? + # --fail exits 183 on findings; any other non-zero is a tool error (e.g. a + # bad regex in .trufflehog-exclude) - surface it instead of a silent red. + if (( rc != 0 && rc != 183 )); then + echo "trufflehog error (exit $rc):"; tail -n 3 "$err" + fi + rm -f "$err" + return $rc +} + +# osv logs "Scanned file and found N packages" - lift those lines into +# a Coverage line so the scan says what was actually parsed, not just what it +# found. Paths are shown relative to the repo. +seckit_coverage() { + local repo="$1" log="$2" + [[ -f "$log" ]] || return 0 + grep -oE 'Scanned .* file and found [0-9]+ packages?' "$log" 2>/dev/null \ + | sed -e "s|Scanned $repo/|Scanned |" \ + | awk 'NR>1{printf "; "} {printf "%s", $0} END{if(NR)print ""}' } # Native mobile source (Swift/Kotlin/ObjC, or an Android manifest) outside @@ -272,14 +294,20 @@ for repo in "${repos[@]}"; do if want osv && have osv-scanner; then echo "${DIM}- osv-scanner (vulnerable deps)${RST}" _seckit_run_and_count osv "$repo" osv-scanner -r "$repo" || hit=1 + last="${RESULTS[$((${#RESULTS[@]} - 1))]}" + cov="$(seckit_coverage "$repo" "${last##*|}")" + [[ -z "$cov" ]] && cov="no package manifests parsed" + echo " ${DIM}coverage: ${cov}${RST}" fi if want gitleaks && [[ -n "$GL" ]]; then echo "${DIM}- gitleaks (secrets in git history)${RST}" if [[ "$GL" == "git" ]]; then - _seckit_run_and_count gitleaks "$repo" gitleaks git "$repo" --redact --no-banner || hit=1 + # --gitleaks-ignore-path: read .gitleaksignore from the scanned repo, + # not from whatever directory seckit happens to be invoked from. + _seckit_run_and_count gitleaks "$repo" gitleaks git "$repo" --redact --no-banner --gitleaks-ignore-path "$repo" || hit=1 else - _seckit_run_and_count gitleaks "$repo" gitleaks detect --source "$repo" --redact --no-banner || hit=1 + _seckit_run_and_count gitleaks "$repo" gitleaks detect --source "$repo" --redact --no-banner --gitleaks-ignore-path "$repo" || hit=1 fi fi @@ -304,7 +332,11 @@ for repo in "${repos[@]}"; do if want checkov && have checkov; then echo "${DIM}- checkov (IaC misconfig)${RST}" - _seckit_run_and_count checkov "$repo" checkov -d "$repo" --quiet --compact --skip-path node_modules || hit=1 + # Accepted findings live in checkov's own baseline format; create with + # `checkov -d . --create-baseline`. Passed only when the file exists. + ck_args=(-d "$repo" --quiet --compact --skip-path node_modules) + [[ -f "$repo/.checkov.baseline" ]] && ck_args+=(--baseline "$repo/.checkov.baseline") + _seckit_run_and_count checkov "$repo" checkov "${ck_args[@]}" || hit=1 fi if want socket && have socket; then @@ -450,6 +482,9 @@ PROMPT_HEAD sev="$(seckit_osv_severity "$lg")" rel="${r#"$ROOT"/}"; [[ "$rel" == "$r" ]] && rel='.' [[ -n "$sev" ]] && printf '**osv severity (`%s`):** %s\n\n' "$rel" "$sev" + cov="$(seckit_coverage "$r" "$lg")" + [[ -z "$cov" ]] && cov="no package manifests parsed" + printf '**Coverage (`%s`):** %s\n\n' "$rel" "$cov" done fi