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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ jobs:
- name: Install shellcheck
run: sudo apt-get update -qq && sudo apt-get install -y shellcheck
- name: Lint shell scripts
run: shellcheck -S warning banner.sh seckit.sh scan_repos.sh scan_skill.sh
run: shellcheck -S warning banner.sh seckit.sh scan_repos.sh scan_skill.sh fix_deps.sh

psscriptanalyzer:
name: PSScriptAnalyzer
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ These rules apply to AI coding agents working in this repository
## Before you commit (definition of green)
- Run `bash check.sh` and make it print `RESULT: green` before committing or
reporting work as done. It runs the same gates CI enforces: bash syntax,
`shellcheck` (banner.sh, seckit.sh, scan_repos.sh, scan_skill.sh, check.sh),
`shellcheck` (banner.sh, seckit.sh, scan_repos.sh, scan_skill.sh, fix_deps.sh, check.sh),
`PSScriptAnalyzer` (all `*.ps1`, recursive), and YAML parse of the CI files.
- A red `check.sh` means a red PR. Fix it; do not hand work back while it fails.
- Keep the shellcheck file list in `check.sh` and `.github/workflows/ci.yml` in
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pwsh ./seckit.ps1 # Windows
| **doctor** | Reports which scanners and clients (jq, yq, gh, az, gitleaks, semgrep, checkov, osv-scanner, trufflehog, pre-commit) are installed and which are missing. |
| **install** | Installs every missing scanner and client via brew/winget/scoop/pipx/npm. No admin rights required — all installs target user space. Run this once on a fresh machine. Missing scanners are skipped gracefully at scan time, so you can scan immediately with whatever is available. |
| **scan** | Sweeps a folder of repos for vulnerable dependencies, code/IaC flaws, malware and secrets. Pick all scanners or a subset (osv, gitleaks, trufflehog, semgrep, checkov, socket). |
| **fix** | Applies safe (same-major) fixes for vulnerable npm dependencies. Lockfile-only by default: never runs install scripts, never touches `node_modules`, never commits. Prints before/after counts and lists what still needs a major upgrade. `--dry-run` previews, `--install` also updates `node_modules`. Other ecosystems are reported as not supported yet. |
| **scan-skill** | Statically vets an AI agent skill or MCP server (directory, `.zip` or git URL) before you install it: prompt-injection, data exfiltration, credential theft, supply-chain RCE, obfuscation, over-broad agency and MCP tool poisoning. Never executes the target; prints a 0-100 risk verdict and a markdown report. |
| **harden** | Drops pre-commit, gitleaks, SECURITY.md, CODEOWNERS, dependabot, CodeQL, the SecKit CI scan workflow and PR templates into a repo so the next commit is clean. Also writes the AI-assistant guardrails that keep secrets out of context: Claude `settings.json` deny rules, a GitHub Copilot content-exclusion file, and a `.cursorignore`. On Node repos it also sets `ignore-scripts=true` in `.npmrc` to block install-time supply-chain worms (see below). |
| **agent** | Installs the SecKit prompt as a Claude subagent, Copilot chat mode, Cursor rule or `AGENTS.md` section so any AI assistant runs the same playbook. |
Expand Down
9 changes: 9 additions & 0 deletions audit.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,15 @@ function Test-GitHubRepoPolicy($Owner, $Repo) {
$vaEnabled = $LASTEXITCODE -eq 0
Add-Result 'required' 'security.vulnerability_alerts' 'enabled' $(if ($vaEnabled) {'enabled'} else {'disabled'}) $(if ($vaEnabled) {'pass'} else {'fail'})

# Dependabot security updates (automatic fix PRs): {"enabled":bool,"paused":bool}.
$asfState = 'unknown'
try {
$asf = (& gh api "repos/$Owner/$Repo/automated-security-fixes" 2>$null) | ConvertFrom-Json
if ($asf.enabled -eq $true) { $asfState = if ($asf.paused -eq $true) { 'paused' } else { 'enabled' } }
elseif ($asf.enabled -eq $false) { $asfState = 'disabled' }
} catch { $asfState = 'unknown' }
Decide 'required' 'security.dependabot_security_updates' 'enabled' $asfState

$sa = $repoObj.security_and_analysis
$secScan = if ($sa -and $sa.secret_scanning) { $sa.secret_scanning.status } else { 'unknown' }
$pushProt = if ($sa -and $sa.secret_scanning_push_protection) { $sa.secret_scanning_push_protection.status } else { 'unknown' }
Expand Down
6 changes: 6 additions & 0 deletions audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ audit_github_repo() {
if [[ "$va_status" == "204" ]]; then record required "security.vulnerability_alerts" "enabled" "enabled" pass
else record required "security.vulnerability_alerts" "enabled" "disabled" fail; fi

# Dependabot security updates (automatic fix PRs): {"enabled":bool,"paused":bool}.
local asf asf_state
asf="$(gh api "repos/$owner/$repo/automated-security-fixes" 2>/dev/null)" || asf='{}'
asf_state="$(jq -r 'if .enabled == true then (if .paused == true then "paused" else "enabled" end) elif .enabled == false then "disabled" else "unknown" end' <<< "$asf")"
match "$asf_state" "enabled" && record required "security.dependabot_security_updates" "enabled" "$asf_state" pass || record required "security.dependabot_security_updates" "enabled" "$asf_state" fail

sec_state="$(jq -r '.security_and_analysis.secret_scanning.status // "unknown"' <<< "$repo_json")"
push_state="$(jq -r '.security_and_analysis.secret_scanning_push_protection.status // "unknown"' <<< "$repo_json")"
cs_state="$(jq -r '.security_and_analysis.code_scanning_default_setup.status // "unknown"' <<< "$repo_json" 2>/dev/null || echo unknown)"
Expand Down
2 changes: 1 addition & 1 deletion check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ set -uo pipefail
cd "$(dirname "$0")" || exit 2

# Keep this list in sync with the shellcheck step in ci.yml.
SH_FILES=(banner.sh seckit.sh scan_repos.sh scan_skill.sh check.sh)
SH_FILES=(banner.sh seckit.sh scan_repos.sh scan_skill.sh fix_deps.sh check.sh)

fail=0
note() { printf '\n== %s ==\n' "$1"; }
Expand Down
20 changes: 19 additions & 1 deletion docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,22 @@ Global level writes `~/.config/git/ignore`, sets `git config --global
core.excludesfile` (only if unset), and adds Claude deny rules to
`~/.claude/settings.json` (or `~/.claude/settings.seckit.json` to merge).

## Fix vulnerable dependencies

```bash
seckit fix . --dry-run # preview: before/after counts, versions that would change
seckit fix . # rewrite package-lock.json only (no scripts, no node_modules)
seckit fix . --install # also update node_modules (still --ignore-scripts)
```

Scope is npm (`package-lock.json`); other lockfiles are listed as not supported
yet. Only same-major updates are applied: `osv-scanner fix` first when
installed, `npm audit fix --package-lock-only --ignore-scripts` after it (and
as the fallback when osv-scanner fails). Anything that needs a major upgrade is
listed with its current and fixed version. SecKit never commits, pushes or
branches; review the diff, run `npm ci --ignore-scripts` and your tests, then
commit. Exit code is 1 while any high/critical vulnerability remains.

## Audit a remote repo or org

`seckit audit` is the read-only posture check. It calls `gh` for GitHub and `az
Expand Down Expand Up @@ -204,7 +220,8 @@ every call is a `GET`.
`seckit enforce` writes the settings flagged by `audit`. It is dry-run by
default and prints every command it would run; pass `--apply` (Bash) or
`-Apply` (PowerShell) to actually write. It enables branch protection on the
default branch, turns on vulnerability alerts + secret scanning + push
default branch, turns on vulnerability alerts + Dependabot security updates +
secret scanning + push
protection + private vulnerability reporting, clamps workflow permissions to
read-only with no PR approval, and drops any missing `SECURITY.md`,
`CODEOWNERS`, dependabot, CodeQL or PR template files into the cwd.
Expand Down Expand Up @@ -322,6 +339,7 @@ The log is written to `~/.seckit/logs/debug-<timestamp>.log` and captures `set -
| `seckit.sh` / `seckit.ps1` | the dispatcher |
| `scan_repos.sh` / `scan_repos.ps1` | the repo scanner |
| `mcp.sh` / `mcp.ps1` | the MCP server manager |
| `fix_deps.sh` / `fix_deps.ps1` | safe (same-major) npm vulnerability fixes |
| `audit.sh` / `audit.ps1` | the read-only GitHub / ADO posture audit |
| `enforce.sh` / `enforce.ps1` | writes the settings flagged by `audit` |
| `banner.sh` | the animated startup banner |
Expand Down
182 changes: 182 additions & 0 deletions fix_deps.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Apply safe (same-major) fixes for vulnerable dependencies.

.DESCRIPTION
Scope: npm (package-lock.json). Other ecosystems are listed as not supported.

Default mode is lockfile-only: package-lock.json (and package.json when npm
must widen a range) is rewritten, node_modules is not touched, and no
install script ever runs. Only non-breaking (same-major) updates are made;
whatever still needs a major upgrade is listed for a human to decide.

Strategy per lockfile: osv-scanner fix --strategy=in-place
--upgrade-config=minor --no-introduce when osv-scanner is installed, then
npm audit fix --package-lock-only --ignore-scripts (also the fallback when
osv-scanner fails, e.g. on lockfiles with file: dependencies).

Never commits, pushes or creates branches. Review the diff yourself.
Exit codes: 0 no high/critical left, 1 high/critical remain, 2 usage error
or npm missing.

.PARAMETER Root
Directory to search for lockfiles (default: current directory).

.PARAMETER DryRun
Show what would change; the target is never written.

.PARAMETER Install
Also update node_modules (npm audit fix, still --ignore-scripts).

.EXAMPLE
./fix_deps.ps1 . -DryRun
./fix_deps.ps1 ~/Git/app
#>
[CmdletBinding()]
param(
[string]$Root = (Get-Location).Path,
[switch]$DryRun,
[switch]$Install
)
$ErrorActionPreference = 'Continue'

function Have($name) { [bool](Get-Command $name -ErrorAction SilentlyContinue) }

if (-not (Test-Path $Root -PathType Container)) { Write-Host "Not a directory: $Root" -ForegroundColor Red; exit 2 }
$Root = (Resolve-Path $Root).Path
$skipDirs = '[\\/](node_modules|\.git|vendor|\.venv)[\\/]'

function Get-Lockfile($Names) {
Get-ChildItem -Path $Root -Recurse -File -Force -ErrorAction SilentlyContinue |
Where-Object { $Names -contains $_.Name -and $_.FullName -notmatch $skipDirs } |
Sort-Object FullName
}
function Get-RelativePath($Path) { [IO.Path]::GetRelativePath($Root, $Path) }

$other = 'yarn.lock', 'pnpm-lock.yaml', 'requirements.txt', 'poetry.lock', 'Pipfile.lock', 'go.mod',
'Cargo.lock', 'Gemfile.lock', 'composer.lock', 'pom.xml', 'build.gradle', 'build.gradle.kts', 'packages.lock.json'
foreach ($f in Get-Lockfile $other) {
Write-Host "not supported yet: $(Get-RelativePath $f.FullName) (fix by hand; seckit scan still reports it)" -ForegroundColor Yellow
}

$locks = @(Get-Lockfile @('package-lock.json'))
if ($locks.Count -eq 0) {
Write-Host "No package-lock.json under $Root. Nothing to fix."
if (Test-Path (Join-Path $Root 'package.json')) { Write-Host 'Generate one first: npm install --package-lock-only --ignore-scripts' -ForegroundColor DarkGray }
exit 0
}
if (-not (Have 'npm')) { Write-Host 'npm is required to fix npm dependencies (install Node.js).' -ForegroundColor Red; exit 2 }

# npm audit JSON for the lockfile in $Dir (no install, no scripts).
function Get-Audit($Dir) {
Push-Location $Dir
try { $raw = (& npm audit --json --package-lock-only 2>$null) -join "`n" } finally { Pop-Location }
try { return $raw | ConvertFrom-Json -AsHashtable } catch { return @{} }
}
function Get-Count($A) {
$v = if ($A.metadata) { $A.metadata.vulnerabilities } else { $null }
if (-not $v) { return 'unknown (npm audit failed)' }
"$($v.critical) critical, $($v.high) high, $($v.moderate) moderate, $($v.low) low"
}
function Get-CritHigh($A) {
if (-not ($A.metadata -and $A.metadata.vulnerabilities)) { return 1 }
[int]$A.metadata.vulnerabilities.critical + [int]$A.metadata.vulnerabilities.high
}
function Read-Lock($Path) { Get-Content $Path -Raw | ConvertFrom-Json -AsHashtable }

$left = 0; $i = 0
$tmp = Join-Path ([IO.Path]::GetTempPath()) "seckit-fix-$PID"
New-Item -ItemType Directory -Force -Path $tmp | Out-Null
try {
foreach ($lock in $locks) {
$i++
$dir = $lock.DirectoryName
Write-Host ''
Write-Host "npm $(Get-RelativePath $lock.FullName)" -ForegroundColor White

$before = Get-Audit $dir
Write-Host " before: $(Get-Count $before)"
$lockBefore = Read-Lock $lock.FullName

$work = $dir
if ($DryRun) {
$pkg = Get-Content (Join-Path $dir 'package.json') -Raw -ErrorAction SilentlyContinue | ConvertFrom-Json -AsHashtable -ErrorAction SilentlyContinue
if ($pkg -and $pkg.workspaces) { Write-Host ' -DryRun does not support npm workspaces yet; skipped' -ForegroundColor Yellow; $left += Get-CritHigh $before; continue }
$work = Join-Path $tmp "work$i"
New-Item -ItemType Directory -Force -Path $work | Out-Null
Copy-Item (Join-Path $dir 'package.json'), $lock.FullName -Destination $work
if (Test-Path (Join-Path $dir '.npmrc')) { Copy-Item (Join-Path $dir '.npmrc') -Destination $work }
}

Push-Location $work
try {
if ($Install -and -not $DryRun) {
Write-Host ' npm audit fix --ignore-scripts (updates node_modules)' -ForegroundColor DarkGray
& npm audit fix --ignore-scripts --no-fund *> $null
} else {
if (Have 'osv-scanner') {
Write-Host ' osv-scanner fix --strategy=in-place --upgrade-config=minor --no-introduce' -ForegroundColor DarkGray
$osvOut = & osv-scanner fix -L (Join-Path $work 'package-lock.json') --strategy=in-place --upgrade-config=minor --no-introduce 2>&1
if ($LASTEXITCODE -ne 0) { Write-Host " osv-scanner fix failed ($(@($osvOut)[-1])); falling back to npm" -ForegroundColor Yellow }
}
Write-Host ' npm audit fix --package-lock-only --ignore-scripts' -ForegroundColor DarkGray
& npm audit fix --package-lock-only --ignore-scripts --no-fund *> $null
}
} finally { Pop-Location }

$after = Get-Audit $work
Write-Host " after: $(Get-Count $after)"

$lockAfter = Read-Lock (Join-Path $work 'package-lock.json')
$pa = if ($lockBefore.packages) { $lockBefore.packages } else { @{} }
$pb = if ($lockAfter.packages) { $lockAfter.packages } else { @{} }
$changes = @(@($pa.Keys) + @($pb.Keys) | Where-Object { $_ } | Sort-Object -Unique | ForEach-Object {
$o = if ($pa[$_]) { $pa[$_].version } else { '-' }
$n = if ($pb[$_]) { $pb[$_].version } else { '-' }
if ($o -ne $n) { "$($_ -replace '^.*node_modules/', '') $o -> $n" }
})
if ($changes.Count) {
Write-Host $(if ($DryRun) { ' would update:' } else { ' updated:' })
$changes | ForEach-Object { Write-Host " $_" }
} else { Write-Host ' no safe update available' }

$rem = @()
if ($after.vulnerabilities) {
foreach ($k in $after.vulnerabilities.Keys) {
$v = $after.vulnerabilities[$k]
$node = if ($v.nodes) { @($v.nodes)[0] } else { "node_modules/$k" }
$cur = if ($pb[$node]) { $pb[$node].version } else { '?' }
$fx = $v.fixAvailable
$rem += if ($fx -is [System.Collections.IDictionary]) {
"$k@$cur [$($v.severity)] -> $($fx.name)@$($fx.version)" + $(if ($fx.isSemVerMajor) { ' (major upgrade)' } else { '' })
} elseif ($fx -eq $true) {
"$k@$cur [$($v.severity)] -> fix in range, run with -Install or npm install"
} else { "$k@$cur [$($v.severity)] -> no fixed version published" }
}
}
if ($rem.Count) {
Write-Host ' still vulnerable (needs a major upgrade or has no fix):' -ForegroundColor Yellow
$rem | Sort-Object -Unique | ForEach-Object { Write-Host " $_" }
}
$left += Get-CritHigh $after

if (-not $DryRun -and (Have 'git')) {
& git -C $dir rev-parse --git-dir *> $null
if ($LASTEXITCODE -eq 0) { & git -C $dir diff --stat -- package.json package-lock.json | ForEach-Object { Write-Host " $_" } }
}
}
} finally { Remove-Item -Recurse -Force $tmp -ErrorAction SilentlyContinue }

Write-Host ''
if ($DryRun) {
Write-Host 'Dry run: nothing was written. Re-run without -DryRun to apply.' -ForegroundColor DarkGray
} else {
Write-Host 'Next steps (SecKit never commits or pushes):' -ForegroundColor White
Write-Host ' 1. Review the diff: git diff -- package.json package-lock.json'
if (-not $Install) { Write-Host ' 2. Install from lock: npm ci --ignore-scripts' }
Write-Host ' 3. Run your build and tests, then commit the change yourself.'
}
if ($left -gt 0) { Write-Host "$left high/critical vulnerabilit(ies) remain." -ForegroundColor Red; exit 1 }
Write-Host 'No high/critical vulnerabilities remain.' -ForegroundColor Green
exit 0
Loading
Loading