ai-project-maintainer 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/README.md +45 -9
  2. package/ai-project-maintainer/agents/openai.yaml +6 -6
  3. package/ai-project-maintainer/references/ci-guardrails.md +55 -55
  4. package/ai-project-maintainer/references/database.md +60 -60
  5. package/ai-project-maintainer/references/electron-desktop.md +43 -43
  6. package/ai-project-maintainer/references/incident-response.md +52 -52
  7. package/ai-project-maintainer/references/security.md +48 -48
  8. package/ai-project-maintainer/references/tool-router.md +53 -53
  9. package/ai-project-maintainer/scripts/bootstrap-local-tools.ps1 +109 -109
  10. package/ai-project-maintainer/scripts/ci-smoke-gate.mjs +26 -26
  11. package/ai-project-maintainer/scripts/init-project.mjs +28 -16
  12. package/ai-project-maintainer/scripts/lib/check-registry.mjs +10 -9
  13. package/ai-project-maintainer/scripts/lib/checks.mjs +22 -10
  14. package/ai-project-maintainer/scripts/lib/command-runner.mjs +17 -3
  15. package/ai-project-maintainer/scripts/lib/policy.mjs +6 -4
  16. package/ai-project-maintainer/scripts/lib/report.mjs +56 -32
  17. package/assets/demo-90s-storyboard.svg +98 -0
  18. package/assets/demo-90s.gif +0 -0
  19. package/assets/social-preview.png +0 -0
  20. package/assets/social-preview.svg +55 -0
  21. package/docs/DEMO.md +39 -44
  22. package/docs/DEMO.zh-CN.md +40 -46
  23. package/docs/GITHUB-LAUNCH-CHECKLIST.md +11 -11
  24. package/docs/POLICY-AND-EXCEPTIONS.zh-CN.md +1 -1
  25. package/docs/PROMOTION.md +49 -21
  26. package/docs/SECURITY-WORKFLOW.md +63 -0
  27. package/docs/UPGRADE-ROADMAP.zh-CN.md +28 -27
  28. package/docs/demo-output/90-second-demo.html +187 -0
  29. package/docs/demo-output/before-after-case.md +91 -0
  30. package/docs/demo-output/security-report.md +45 -37
  31. package/docs/superpowers/plans/2026-06-29-ci-dogfooding.md +200 -200
  32. package/examples/demo-ai-app/.ai-maintainer/business-flows.yml +14 -0
  33. package/examples/demo-ai-app/.ai-maintainer/db-migration-policy.yml +6 -0
  34. package/examples/demo-ai-app/.ai-maintainer/evidence-sources.yml +18 -0
  35. package/examples/demo-ai-app/.ai-maintainer/exceptions.yml +1 -0
  36. package/examples/demo-ai-app/.ai-maintainer/incident-runbook.md +11 -0
  37. package/examples/demo-ai-app/.ai-maintainer/observability-checklist.yml +7 -0
  38. package/examples/demo-ai-app/.ai-maintainer/policy.yml +27 -0
  39. package/examples/demo-ai-app/.ai-maintainer/project-profile.yml +15 -0
  40. package/examples/demo-ai-app/.ai-maintainer/release-checklist.yml +7 -0
  41. package/examples/demo-ai-app/.ai-maintainer/risk-policy.yml +5 -0
  42. package/examples/demo-ai-app/.ai-maintainer/threat-model.md +18 -0
  43. package/examples/demo-ai-app/README.md +38 -0
  44. package/examples/demo-ai-app/package-lock.json +15 -0
  45. package/examples/demo-ai-app/package.json +16 -0
  46. package/examples/demo-ai-app/scripts/build.mjs +18 -0
  47. package/examples/demo-ai-app/scripts/create-before-state.mjs +86 -0
  48. package/examples/demo-ai-app/scripts/run-demo-gate.mjs +95 -0
  49. package/examples/demo-ai-app/src/order-risk.js +28 -0
  50. package/examples/demo-ai-app/test/order-risk.test.mjs +24 -0
  51. package/package.json +11 -3
@@ -1,53 +1,53 @@
1
- # Tool Router
2
-
3
- Use this reference to choose checks dynamically. Run only tools that match the repository and the user's scope.
4
-
5
- ## Selection Order
6
-
7
- 1. Inspect the project with `scripts/probe-project.mjs`.
8
- 2. Use installed Codex skills for security review when available.
9
- 3. Use local CLI tools that are already installed.
10
- 4. Use package-manager or containerized tools only when the repo already uses them or the user approves.
11
- 5. Fall back to manual review with explicit coverage gaps.
12
-
13
- ## Core Routing Table
14
-
15
- | Surface | Signals | Primary tools | Notes |
16
- | --- | --- | --- | --- |
17
- | Code security | Auth, API routes, serializers, file upload, HTTP clients, SQL construction, crypto | `codex-security`, Semgrep, CodeQL, Open Code Review | Prioritize changed files and dataflow into sinks. |
18
- | Secrets | `.env`, private keys, token-like config, CI variables, committed credentials | Gitleaks, Trivy secret scan | Do not print secret values. Report only location and type. |
19
- | Dependencies | Lockfiles, Docker images, SBOM, package manifests | Trivy, OSV-Scanner, native package audit | Separate reachable production risk from dev-only noise. |
20
- | Database migrations | `migrations/`, `db/migrate`, Prisma, Drizzle, Alembic, Flyway, Liquibase, raw SQL | Squawk, Atlas, Bytebase, pgroll, strong_migrations, gh-ost, pt-online-schema-change | Review lock behavior and rollback before correctness polish. |
21
- | IaC and cloud | Terraform, CloudFormation, Pulumi, Helm, Kubernetes YAML, Docker Compose | Checkov, Trivy config, Conftest/OPA, Kubescape | Focus on public exposure, IAM, network policy, privileged runtime, and secret mounts. |
22
- | Containers | Dockerfile, compose, image references, deploy manifests | Trivy image/config, Hadolint, Docker build checks | Check root user, privileged mode, writable filesystem, and unpinned base images. |
23
- | Kubernetes runtime | K8s manifests, `kubectl` context, incident request | k8sgpt, HolmesGPT, Kubescape, Falco, Cilium/Tetragon | Default to read-only commands. |
24
- | Web/API DAST | Staging URL explicitly provided by user | OWASP ZAP, Nuclei | Require explicit scope for active scans. Never infer internet targets. |
25
- | Incident response | Alerts, logs, metrics, traces, rollout history, deploys, migrations | HolmesGPT, OpenSRE, k8sgpt, kubectl, observability CLIs/APIs | Build a timeline before proposing fixes. |
26
-
27
- ## Command Patterns
28
-
29
- Use these as patterns, adapting paths and package managers to the repo.
30
-
31
- ```bash
32
- semgrep scan --config auto
33
- trivy fs --scanners vuln,secret,misconfig .
34
- gitleaks detect --source . --redact
35
- checkov -d .
36
- squawk path/to/migration.sql
37
- atlas migrate lint --dir "file://migrations"
38
- kubescape scan
39
- k8sgpt analyze
40
- ```
41
-
42
- Use CodeQL when a CodeQL database or GitHub code scanning setup already exists, or when the user asks for deeper security analysis.
43
-
44
- ## Missing Tool Handling
45
-
46
- When a useful tool is unavailable, write a short gap:
47
-
48
- ```text
49
- Unavailable: squawk. Coverage gap: Postgres migration lock-risk linting was not run.
50
- Smallest next guardrail: add squawk to CI for changed `.sql` migration files.
51
- ```
52
-
53
- Do not turn the review into an installation project unless the user asks for setup.
1
+ # Tool Router
2
+
3
+ Use this reference to choose checks dynamically. Run only tools that match the repository and the user's scope.
4
+
5
+ ## Selection Order
6
+
7
+ 1. Inspect the project with `scripts/probe-project.mjs`.
8
+ 2. Use installed Codex skills for security review when available.
9
+ 3. Use local CLI tools that are already installed.
10
+ 4. Use package-manager or containerized tools only when the repo already uses them or the user approves.
11
+ 5. Fall back to manual review with explicit coverage gaps.
12
+
13
+ ## Core Routing Table
14
+
15
+ | Surface | Signals | Primary tools | Notes |
16
+ | --- | --- | --- | --- |
17
+ | Code security | Auth, API routes, serializers, file upload, HTTP clients, SQL construction, crypto | `codex-security`, Semgrep, CodeQL, Open Code Review | Prioritize changed files and dataflow into sinks. |
18
+ | Secrets | `.env`, private keys, token-like config, CI variables, committed credentials | Gitleaks, Trivy secret scan | Do not print secret values. Report only location and type. |
19
+ | Dependencies | Lockfiles, Docker images, SBOM, package manifests | Trivy, OSV-Scanner, native package audit | Separate reachable production risk from dev-only noise. |
20
+ | Database migrations | `migrations/`, `db/migrate`, Prisma, Drizzle, Alembic, Flyway, Liquibase, raw SQL | Squawk, Atlas, Bytebase, pgroll, strong_migrations, gh-ost, pt-online-schema-change | Review lock behavior and rollback before correctness polish. |
21
+ | IaC and cloud | Terraform, CloudFormation, Pulumi, Helm, Kubernetes YAML, Docker Compose | Checkov, Trivy config, Conftest/OPA, Kubescape | Focus on public exposure, IAM, network policy, privileged runtime, and secret mounts. |
22
+ | Containers | Dockerfile, compose, image references, deploy manifests | Trivy image/config, Hadolint, Docker build checks | Check root user, privileged mode, writable filesystem, and unpinned base images. |
23
+ | Kubernetes runtime | K8s manifests, `kubectl` context, incident request | k8sgpt, HolmesGPT, Kubescape, Falco, Cilium/Tetragon | Default to read-only commands. |
24
+ | Web/API DAST | Staging URL explicitly provided by user | OWASP ZAP, Nuclei | Require explicit scope for active scans. Never infer internet targets. |
25
+ | Incident response | Alerts, logs, metrics, traces, rollout history, deploys, migrations | HolmesGPT, OpenSRE, k8sgpt, kubectl, observability CLIs/APIs | Build a timeline before proposing fixes. |
26
+
27
+ ## Command Patterns
28
+
29
+ Use these as patterns, adapting paths and package managers to the repo.
30
+
31
+ ```bash
32
+ semgrep scan --config auto
33
+ trivy fs --scanners vuln,secret,misconfig .
34
+ gitleaks detect --source . --redact
35
+ checkov -d .
36
+ squawk path/to/migration.sql
37
+ atlas migrate lint --dir "file://migrations"
38
+ kubescape scan
39
+ k8sgpt analyze
40
+ ```
41
+
42
+ Use CodeQL when a CodeQL database or GitHub code scanning setup already exists, or when the user asks for deeper security analysis.
43
+
44
+ ## Missing Tool Handling
45
+
46
+ When a useful tool is unavailable, write a short gap:
47
+
48
+ ```text
49
+ Unavailable: squawk. Coverage gap: Postgres migration lock-risk linting was not run.
50
+ Smallest next guardrail: add squawk to CI for changed `.sql` migration files.
51
+ ```
52
+
53
+ Do not turn the review into an installation project unless the user asks for setup.
@@ -1,109 +1,109 @@
1
- param(
2
- [string[]]$Tools = @("gitleaks", "trivy", "squawk"),
3
- [string]$InstallDir = "$env:USERPROFILE\.codex\security-tools",
4
- [switch]$AddToPath
5
- )
6
-
7
- $ErrorActionPreference = "Stop"
8
- $binDir = Join-Path $InstallDir "bin"
9
- New-Item -ItemType Directory -Force -Path $binDir | Out-Null
10
- $requestedTools = @()
11
- foreach ($item in $Tools) {
12
- $requestedTools += ($item -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
13
- }
14
-
15
- function Write-Step($Message) {
16
- Write-Host "==> $Message"
17
- }
18
-
19
- function Test-Command($Name) {
20
- $null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
21
- }
22
-
23
- function Get-Asset($Repo, [string[]]$Patterns) {
24
- $release = Invoke-RestMethod -Headers @{ "User-Agent" = "ai-project-maintainer" } -Uri "https://api.github.com/repos/$Repo/releases/latest"
25
- foreach ($pattern in $Patterns) {
26
- $asset = $release.assets | Where-Object { $_.name -match $pattern -and $_.name -match '\.zip$' } | Select-Object -First 1
27
- if ($asset) { return $asset }
28
- }
29
- throw "No Windows zip asset matched $($Patterns -join ', ') for $Repo"
30
- }
31
-
32
- function Install-FromGitHubZip($Name, $Repo, [string[]]$Patterns, $ExeName) {
33
- if (Test-Command $ExeName) {
34
- Write-Step "$Name already available on PATH"
35
- return
36
- }
37
-
38
- $asset = Get-Asset -Repo $Repo -Patterns $Patterns
39
- $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("$Name-" + [guid]::NewGuid())
40
- $zip = Join-Path $tmp $asset.name
41
- New-Item -ItemType Directory -Force -Path $tmp | Out-Null
42
-
43
- Write-Step "Downloading $Name from $Repo ($($asset.name))"
44
- Invoke-WebRequest -Headers @{ "User-Agent" = "ai-project-maintainer" } -Uri $asset.browser_download_url -OutFile $zip
45
- Expand-Archive -LiteralPath $zip -DestinationPath $tmp -Force
46
-
47
- $exe = Get-ChildItem -Path $tmp -Recurse -File -Filter "$ExeName.exe" | Select-Object -First 1
48
- if (-not $exe) { throw "Downloaded $Name but could not find $ExeName.exe in archive" }
49
-
50
- Copy-Item -LiteralPath $exe.FullName -Destination (Join-Path $binDir "$ExeName.exe") -Force
51
- Write-Step "Installed $Name to $binDir"
52
- }
53
-
54
- function Install-WithUvTool($Name, $Package, $ExeName) {
55
- if (Test-Command $ExeName) {
56
- Write-Step "$Name already available on PATH"
57
- return
58
- }
59
- if (-not (Test-Command "uv")) {
60
- Write-Warning "uv is not installed; cannot install $Name locally. Install uv, Python, or Docker and rerun."
61
- return
62
- }
63
- Write-Step "Installing $Name with uv tool install $Package"
64
- & uv tool install $Package
65
- if ($LASTEXITCODE -ne 0) {
66
- throw "uv tool install $Package failed with exit code $LASTEXITCODE"
67
- }
68
- }
69
-
70
- foreach ($tool in $requestedTools) {
71
- try {
72
- switch ($tool.ToLowerInvariant()) {
73
- "gitleaks" {
74
- Install-FromGitHubZip -Name "gitleaks" -Repo "gitleaks/gitleaks" -Patterns @("windows.*x64", "windows.*64") -ExeName "gitleaks"
75
- }
76
- "trivy" {
77
- Install-FromGitHubZip -Name "trivy" -Repo "aquasecurity/trivy" -Patterns @("windows-64bit", "windows.*64") -ExeName "trivy"
78
- }
79
- "squawk" {
80
- Install-FromGitHubZip -Name "squawk" -Repo "sbdchd/squawk" -Patterns @("windows.*x86_64", "windows.*amd64", "windows.*64") -ExeName "squawk"
81
- }
82
- "semgrep" {
83
- Install-WithUvTool -Name "semgrep" -Package "semgrep" -ExeName "semgrep"
84
- }
85
- "checkov" {
86
- Install-WithUvTool -Name "checkov" -Package "checkov" -ExeName "checkov"
87
- }
88
- default {
89
- Write-Warning "Unknown local bootstrap tool '$tool'. Supported: gitleaks, trivy, squawk, semgrep, checkov."
90
- }
91
- }
92
- } catch {
93
- Write-Warning "Could not install ${tool}: $($_.Exception.Message)"
94
- }
95
- }
96
-
97
- if ($AddToPath) {
98
- $current = [Environment]::GetEnvironmentVariable("Path", "User")
99
- $parts = $current -split ';' | Where-Object { $_ }
100
- if ($parts -notcontains $binDir) {
101
- [Environment]::SetEnvironmentVariable("Path", ($parts + $binDir -join ';'), "User")
102
- Write-Step "Added $binDir to the user PATH. Restart terminals/Codex to pick it up."
103
- } else {
104
- Write-Step "$binDir is already in the user PATH"
105
- }
106
- }
107
-
108
- Write-Step "Installed tools:"
109
- Get-ChildItem -File -LiteralPath $binDir | Select-Object Name,Length,LastWriteTime
1
+ param(
2
+ [string[]]$Tools = @("gitleaks", "trivy", "squawk"),
3
+ [string]$InstallDir = "$env:USERPROFILE\.codex\security-tools",
4
+ [switch]$AddToPath
5
+ )
6
+
7
+ $ErrorActionPreference = "Stop"
8
+ $binDir = Join-Path $InstallDir "bin"
9
+ New-Item -ItemType Directory -Force -Path $binDir | Out-Null
10
+ $requestedTools = @()
11
+ foreach ($item in $Tools) {
12
+ $requestedTools += ($item -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ })
13
+ }
14
+
15
+ function Write-Step($Message) {
16
+ Write-Host "==> $Message"
17
+ }
18
+
19
+ function Test-Command($Name) {
20
+ $null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
21
+ }
22
+
23
+ function Get-Asset($Repo, [string[]]$Patterns) {
24
+ $release = Invoke-RestMethod -Headers @{ "User-Agent" = "ai-project-maintainer" } -Uri "https://api.github.com/repos/$Repo/releases/latest"
25
+ foreach ($pattern in $Patterns) {
26
+ $asset = $release.assets | Where-Object { $_.name -match $pattern -and $_.name -match '\.zip$' } | Select-Object -First 1
27
+ if ($asset) { return $asset }
28
+ }
29
+ throw "No Windows zip asset matched $($Patterns -join ', ') for $Repo"
30
+ }
31
+
32
+ function Install-FromGitHubZip($Name, $Repo, [string[]]$Patterns, $ExeName) {
33
+ if (Test-Command $ExeName) {
34
+ Write-Step "$Name already available on PATH"
35
+ return
36
+ }
37
+
38
+ $asset = Get-Asset -Repo $Repo -Patterns $Patterns
39
+ $tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("$Name-" + [guid]::NewGuid())
40
+ $zip = Join-Path $tmp $asset.name
41
+ New-Item -ItemType Directory -Force -Path $tmp | Out-Null
42
+
43
+ Write-Step "Downloading $Name from $Repo ($($asset.name))"
44
+ Invoke-WebRequest -Headers @{ "User-Agent" = "ai-project-maintainer" } -Uri $asset.browser_download_url -OutFile $zip
45
+ Expand-Archive -LiteralPath $zip -DestinationPath $tmp -Force
46
+
47
+ $exe = Get-ChildItem -Path $tmp -Recurse -File -Filter "$ExeName.exe" | Select-Object -First 1
48
+ if (-not $exe) { throw "Downloaded $Name but could not find $ExeName.exe in archive" }
49
+
50
+ Copy-Item -LiteralPath $exe.FullName -Destination (Join-Path $binDir "$ExeName.exe") -Force
51
+ Write-Step "Installed $Name to $binDir"
52
+ }
53
+
54
+ function Install-WithUvTool($Name, $Package, $ExeName) {
55
+ if (Test-Command $ExeName) {
56
+ Write-Step "$Name already available on PATH"
57
+ return
58
+ }
59
+ if (-not (Test-Command "uv")) {
60
+ Write-Warning "uv is not installed; cannot install $Name locally. Install uv, Python, or Docker and rerun."
61
+ return
62
+ }
63
+ Write-Step "Installing $Name with uv tool install $Package"
64
+ & uv tool install $Package
65
+ if ($LASTEXITCODE -ne 0) {
66
+ throw "uv tool install $Package failed with exit code $LASTEXITCODE"
67
+ }
68
+ }
69
+
70
+ foreach ($tool in $requestedTools) {
71
+ try {
72
+ switch ($tool.ToLowerInvariant()) {
73
+ "gitleaks" {
74
+ Install-FromGitHubZip -Name "gitleaks" -Repo "gitleaks/gitleaks" -Patterns @("windows.*x64", "windows.*64") -ExeName "gitleaks"
75
+ }
76
+ "trivy" {
77
+ Install-FromGitHubZip -Name "trivy" -Repo "aquasecurity/trivy" -Patterns @("windows-64bit", "windows.*64") -ExeName "trivy"
78
+ }
79
+ "squawk" {
80
+ Install-FromGitHubZip -Name "squawk" -Repo "sbdchd/squawk" -Patterns @("windows.*x86_64", "windows.*amd64", "windows.*64") -ExeName "squawk"
81
+ }
82
+ "semgrep" {
83
+ Install-WithUvTool -Name "semgrep" -Package "semgrep" -ExeName "semgrep"
84
+ }
85
+ "checkov" {
86
+ Install-WithUvTool -Name "checkov" -Package "checkov" -ExeName "checkov"
87
+ }
88
+ default {
89
+ Write-Warning "Unknown local bootstrap tool '$tool'. Supported: gitleaks, trivy, squawk, semgrep, checkov."
90
+ }
91
+ }
92
+ } catch {
93
+ Write-Warning "Could not install ${tool}: $($_.Exception.Message)"
94
+ }
95
+ }
96
+
97
+ if ($AddToPath) {
98
+ $current = [Environment]::GetEnvironmentVariable("Path", "User")
99
+ $parts = $current -split ';' | Where-Object { $_ }
100
+ if ($parts -notcontains $binDir) {
101
+ [Environment]::SetEnvironmentVariable("Path", ($parts + $binDir -join ';'), "User")
102
+ Write-Step "Added $binDir to the user PATH. Restart terminals/Codex to pick it up."
103
+ } else {
104
+ Write-Step "$binDir is already in the user PATH"
105
+ }
106
+ }
107
+
108
+ Write-Step "Installed tools:"
109
+ Get-ChildItem -File -LiteralPath $binDir | Select-Object Name,Length,LastWriteTime
@@ -1,26 +1,26 @@
1
- #!/usr/bin/env node
2
- import path from "node:path";
3
- import { runLocalGate } from "./run-local-gate.mjs";
4
-
5
- const projectRoot = path.resolve(process.argv[2] || process.cwd());
6
- const outputPath = process.argv[3] || "reports/security-report.json";
7
-
8
- const report = runLocalGate(projectRoot, {
9
- noTests: true,
10
- outputPath,
11
- writeReports: true,
12
- runnerOptions: {
13
- envPath: "",
14
- },
15
- });
16
-
17
- const summary = {
18
- status: report.passed ? "PASS" : "FAIL",
19
- blockers: report.summary?.blockers ?? 0,
20
- warnings: report.summary?.warnings ?? 0,
21
- coverageGaps: report.summary?.coverageGaps ?? 0,
22
- outputPath,
23
- };
24
-
25
- console.log(JSON.stringify(summary, null, 2));
26
- process.exit(report.passed ? 0 : 1);
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { runLocalGate } from "./run-local-gate.mjs";
4
+
5
+ const projectRoot = path.resolve(process.argv[2] || process.cwd());
6
+ const outputPath = process.argv[3] || "reports/security-report.json";
7
+
8
+ const report = runLocalGate(projectRoot, {
9
+ noTests: true,
10
+ outputPath,
11
+ writeReports: true,
12
+ runnerOptions: {
13
+ envPath: "",
14
+ },
15
+ });
16
+
17
+ const summary = {
18
+ status: report.passed ? "PASS" : "FAIL",
19
+ blockers: report.summary?.blockers ?? 0,
20
+ warnings: report.summary?.warnings ?? 0,
21
+ coverageGaps: report.summary?.coverageGaps ?? 0,
22
+ outputPath,
23
+ };
24
+
25
+ console.log(JSON.stringify(summary, null, 2));
26
+ process.exit(report.passed ? 0 : 1);
@@ -34,7 +34,7 @@ warn_on:
34
34
 
35
35
  const exceptionsTemplate = `exceptions:
36
36
  - id: "example-dev-only-vuln"
37
- check: "npm audit"
37
+ check: "package-audit"
38
38
  reason: "dev-only transitive dependency, not shipped"
39
39
  expires: "2026-09-01"
40
40
  owner: "repo-owner"
@@ -91,20 +91,32 @@ jobs:
91
91
  npm install
92
92
  fi
93
93
 
94
- - name: Install security scanners
95
- shell: bash
96
- run: |
97
- set -euo pipefail
98
- mkdir -p "$HOME/.local/bin"
99
- echo "$HOME/.local/bin" >> "$GITHUB_PATH"
100
- echo "$HOME/go/bin" >> "$GITHUB_PATH"
101
- python -m pip install --user semgrep zizmor checkov
102
- go install github.com/gitleaks/gitleaks/v8@latest
103
- go install github.com/google/osv-scanner/cmd/osv-scanner@latest
104
- go install github.com/rhysd/actionlint/cmd/actionlint@latest
105
- go install github.com/anchore/syft/cmd/syft@latest
106
- go install github.com/anchore/grype/cmd/grype@latest
107
- curl -sSfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b "$HOME/.local/bin"
94
+ - name: Install security scanners
95
+ shell: bash
96
+ env:
97
+ GITLEAKS_VERSION: v8.30.0
98
+ TRIVY_VERSION: v0.71.2
99
+ SEMGREP_VERSION: 1.168.0
100
+ ZIZMOR_VERSION: 1.26.1
101
+ OSV_SCANNER_VERSION: v2.4.0
102
+ ACTIONLINT_VERSION: v1.7.12
103
+ SYFT_VERSION: v1.46.0
104
+ GRYPE_VERSION: v0.111.1
105
+ CHECKOV_VERSION: 3.3.2
106
+ SCORECARD_VERSION: v5.3.0
107
+ run: |
108
+ set -euo pipefail
109
+ mkdir -p "$HOME/.local/bin"
110
+ echo "$HOME/.local/bin" >> "$GITHUB_PATH"
111
+ echo "$HOME/go/bin" >> "$GITHUB_PATH"
112
+ python -m pip install --user "semgrep==$SEMGREP_VERSION" "zizmor==$ZIZMOR_VERSION" "checkov==$CHECKOV_VERSION"
113
+ go install "github.com/zricethezav/gitleaks/v8@$GITLEAKS_VERSION"
114
+ go install "github.com/google/osv-scanner/v2/cmd/osv-scanner@$OSV_SCANNER_VERSION"
115
+ go install "github.com/rhysd/actionlint/cmd/actionlint@$ACTIONLINT_VERSION"
116
+ go install "github.com/anchore/syft/cmd/syft@$SYFT_VERSION"
117
+ go install "github.com/anchore/grype/cmd/grype@$GRYPE_VERSION"
118
+ go install "github.com/ossf/scorecard/v5@$SCORECARD_VERSION"
119
+ curl -sSfL "https://raw.githubusercontent.com/aquasecurity/trivy/$TRIVY_VERSION/contrib/install.sh" | sh -s -- -b "$HOME/.local/bin" "$TRIVY_VERSION"
108
120
 
109
121
  - name: Checkout AI Project Maintainer
110
122
  shell: bash
@@ -134,7 +146,7 @@ jobs:
134
146
 
135
147
  - name: Upload SARIF to code scanning
136
148
  if: always()
137
- uses: github/codeql-action/upload-sarif@v3
149
+ uses: github/codeql-action/upload-sarif@v4
138
150
  with:
139
151
  sarif_file: reports/security-report.sarif
140
152
  continue-on-error: true
@@ -1,7 +1,7 @@
1
- import {
2
- runCiSecurityChecks,
3
- runDatabaseChecks,
4
- runElectronChecks,
1
+ import {
2
+ runActionlintChecks,
3
+ runDatabaseChecks,
4
+ runElectronChecks,
5
5
  runGrypeChecks,
6
6
  runIacChecks,
7
7
  runMegaLinterChecks,
@@ -12,9 +12,10 @@ import {
12
12
  runScorecardChecks,
13
13
  runSecretChecks,
14
14
  runSyftChecks,
15
- runTestChecks,
16
- runTrivyFilesystemChecks,
17
- } from "./checks.mjs";
15
+ runTestChecks,
16
+ runTrivyFilesystemChecks,
17
+ runZizmorChecks,
18
+ } from "./checks.mjs";
18
19
 
19
20
  const builtinCheckRegistry = [
20
21
  { id: "tests", group: "tests", title: "Project tests and release scripts", requiredTools: ["npm", "pnpm", "yarn", "bun"], detect: (project) => Boolean(project.packageJson), run: ({ project, options }) => runTestChecks(project, options), defaultLevel: "block" },
@@ -25,8 +26,8 @@ const builtinCheckRegistry = [
25
26
  { id: "semgrep", group: "sast", title: "Semgrep static scan", requiredTools: ["semgrep"], detect: () => true, run: ({ project, options }) => runSastChecks(project, options), defaultLevel: "block" },
26
27
  { id: "syft", group: "supply-chain", title: "Syft SBOM", requiredTools: ["syft"], detect: () => true, run: ({ project, options }) => runSyftChecks(project, options), defaultLevel: "warn" },
27
28
  { id: "grype", group: "supply-chain", title: "Grype vulnerability scan", requiredTools: ["grype"], detect: () => true, run: ({ project, options }) => runGrypeChecks(project, options), defaultLevel: "warn" },
28
- { id: "actionlint", group: "ci-security", title: "actionlint workflow lint", requiredTools: ["actionlint"], detect: (project) => (project.riskSurfaces?.ci || []).length > 0, run: ({ project, options }) => runCiSecurityChecks(project, options).filter((check) => check.checkId === "actionlint"), defaultLevel: "block" },
29
- { id: "zizmor", group: "ci-security", title: "zizmor workflow security", requiredTools: ["zizmor"], detect: (project) => (project.riskSurfaces?.ci || []).length > 0, run: ({ project, options }) => runCiSecurityChecks(project, options).filter((check) => check.checkId === "zizmor"), defaultLevel: "warn" },
29
+ { id: "actionlint", group: "ci-security", title: "actionlint workflow lint", requiredTools: ["actionlint"], detect: (project) => (project.riskSurfaces?.ci || []).length > 0, run: ({ project, options }) => runActionlintChecks(project, options), defaultLevel: "block" },
30
+ { id: "zizmor", group: "ci-security", title: "zizmor workflow security", requiredTools: ["zizmor"], detect: (project) => (project.riskSurfaces?.ci || []).length > 0, run: ({ project, options }) => runZizmorChecks(project, options), defaultLevel: "warn" },
30
31
  { id: "checkov", group: "iac", title: "Checkov IaC scan", requiredTools: ["checkov"], detect: (project) => (project.riskSurfaces?.infra || []).length > 0, run: ({ project, options }) => runIacChecks(project, options).filter((check) => check.checkId === "checkov" || check.checkId === "trivy-config"), defaultLevel: "warn" },
31
32
  { id: "electron", group: "electron", title: "Electron baseline", requiredTools: [], detect: (project) => Boolean(project.electron?.detected), run: ({ project }) => runElectronChecks(project), defaultLevel: "block" },
32
33
  { id: "database", group: "database", title: "Database migration review", requiredTools: ["squawk"], detect: (project) => (project.riskSurfaces?.database || []).length > 0, run: ({ project, options }) => runDatabaseChecks(project, options), defaultLevel: "block" },
@@ -191,16 +191,28 @@ export function runGrypeChecks(project, options = {}) {
191
191
  ];
192
192
  }
193
193
 
194
- export function runCiSecurityChecks(project, options = {}) {
195
- if (!(project.riskSurfaces?.ci || []).length) return [];
196
- const runner = options.runnerOptions || {};
197
- const actionlint = runCommand("actionlint", [], { ...runner, cwd: project.root, timeoutMs: 5 * 60 * 1000 });
198
- const zizmor = runCommand("zizmor", [".github/workflows"], { ...runner, cwd: project.root, timeoutMs: 5 * 60 * 1000 });
199
- return [
200
- makeCheck("actionlint workflow lint", "ci-security", actionlint, actionlint.status === "fail", "GitHub Actions workflow syntax and common mistakes must be fixed.", { checkId: "actionlint" }),
201
- makeCheck("zizmor workflow security", "ci-security", zizmor, zizmor.status === "fail", "High-risk GitHub Actions patterns must be fixed.", { checkId: "zizmor" }),
202
- ];
203
- }
194
+ export function runCiSecurityChecks(project, options = {}) {
195
+ if (!(project.riskSurfaces?.ci || []).length) return [];
196
+ return [...runActionlintChecks(project, options), ...runZizmorChecks(project, options)];
197
+ }
198
+
199
+ export function runActionlintChecks(project, options = {}) {
200
+ if (!(project.riskSurfaces?.ci || []).length) return [];
201
+ const runner = options.runnerOptions || {};
202
+ const actionlint = runCommand("actionlint", [], { ...runner, cwd: project.root, timeoutMs: 5 * 60 * 1000 });
203
+ return [
204
+ makeCheck("actionlint workflow lint", "ci-security", actionlint, actionlint.status === "fail", "GitHub Actions workflow syntax and common mistakes must be fixed.", { checkId: "actionlint" }),
205
+ ];
206
+ }
207
+
208
+ export function runZizmorChecks(project, options = {}) {
209
+ if (!(project.riskSurfaces?.ci || []).length) return [];
210
+ const runner = options.runnerOptions || {};
211
+ const zizmor = runCommand("zizmor", [".github/workflows"], { ...runner, cwd: project.root, timeoutMs: 5 * 60 * 1000 });
212
+ return [
213
+ makeCheck("zizmor workflow security", "ci-security", zizmor, zizmor.status === "fail", "High-risk GitHub Actions patterns must be fixed.", { checkId: "zizmor" }),
214
+ ];
215
+ }
204
216
 
205
217
  export function runIacChecks(project, options = {}) {
206
218
  if (!(project.riskSurfaces?.infra || []).length) return [];
@@ -78,9 +78,23 @@ function spawnTarget(resolved, args) {
78
78
  };
79
79
  }
80
80
 
81
- export function runCommand(command, commandArgs = [], options = {}) {
82
- const resolved = commandPath(command, options);
83
- const commandText = [command, ...commandArgs].join(" ");
81
+ export function runCommand(command, commandArgs = [], options = {}) {
82
+ if (typeof options.commandRunner === "function") {
83
+ const started = Date.now();
84
+ const commandText = [command, ...commandArgs].join(" ");
85
+ const result = options.commandRunner(command, commandArgs, options) || {};
86
+ return {
87
+ status: result.status || (result.code === 0 ? "pass" : "fail"),
88
+ command: result.command || commandText,
89
+ stdout: tail(result.stdout),
90
+ stderr: tail(result.stderr || result.error?.message || ""),
91
+ code: result.code ?? null,
92
+ durationMs: result.durationMs ?? Date.now() - started,
93
+ };
94
+ }
95
+
96
+ const resolved = commandPath(command, options);
97
+ const commandText = [command, ...commandArgs].join(" ");
84
98
  if (!resolved) {
85
99
  return {
86
100
  status: "missing",
@@ -101,10 +101,12 @@ export function validateExceptions(exceptions, now = new Date()) {
101
101
  return { valid, invalid };
102
102
  }
103
103
 
104
- function matchesException(check, exception) {
105
- const target = String(exception.check || "").toLowerCase();
106
- return target === String(check.name || "").toLowerCase() || target === String(check.group || "").toLowerCase();
107
- }
104
+ function matchesException(check, exception) {
105
+ const target = String(exception.check || "").toLowerCase();
106
+ return [check.checkId, check.name, check.group]
107
+ .map((value) => String(value || "").toLowerCase())
108
+ .includes(target);
109
+ }
108
110
 
109
111
  function policyKeyForCheck(check) {
110
112
  if (check.group === "tests") return "tests";