Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
45 changes: 40 additions & 5 deletions scan_repos.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,17 @@ function Test-CleanWarning([string]$Key, [string]$Log) {
return $false
}

# osv logs "Scanned <path> 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) {
Expand Down Expand Up @@ -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)) {
Expand All @@ -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)) {
Expand Down Expand Up @@ -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()
}
}

Expand Down
43 changes: 39 additions & 4 deletions scan_repos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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 <path> 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
Expand Down Expand Up @@ -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

Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down
Loading