hullproof 0.0.0-stage → 0.5.0
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.
- package/LICENSE +222 -0
- package/NOTICE +32 -0
- package/README.md +313 -2
- package/checks/db.firebase-rules-not-open.json +24 -0
- package/checks/db.rls-every-table.json +25 -0
- package/checks/log.no-credential-in-logs.json +30 -0
- package/checks/secrets.no-env-file-committed.json +31 -0
- package/checks/secrets.no-secret-in-public-env.json +37 -0
- package/checks/storage.firebase-rules-not-open.json +22 -0
- package/checks/supply.no-untrusted-event-in-run.json +23 -0
- package/checks/web.cors-no-reflected-origin.json +31 -0
- package/detectors/ai.json +25 -0
- package/detectors/auth.json +27 -0
- package/detectors/ci.json +19 -0
- package/detectors/containers.json +8 -0
- package/detectors/data_stores.json +38 -0
- package/detectors/frameworks.json +94 -0
- package/detectors/hosting.json +44 -0
- package/detectors/iac.json +43 -0
- package/detectors/languages.json +48 -0
- package/detectors/mobile.json +22 -0
- package/detectors/package_managers.json +39 -0
- package/detectors/payments.json +19 -0
- package/detectors/runtimes.json +17 -0
- package/dist/checks/code/admin-role.js +46 -0
- package/dist/checks/code/owner-filter.js +91 -0
- package/dist/checks/code/shared.js +84 -0
- package/dist/checks/code/webhook-verify.js +77 -0
- package/dist/checks/context.js +64 -0
- package/dist/checks/map/config-checks.js +74 -0
- package/dist/checks/map/entry-checks.js +97 -0
- package/dist/checks/map/shared.js +44 -0
- package/dist/checks/ops.js +307 -0
- package/dist/checks/pro-map.js +1 -0
- package/dist/checks/run.js +92 -0
- package/dist/checks/types.js +1 -0
- package/dist/ci/comment.js +44 -0
- package/dist/ci/sarif-check.js +157 -0
- package/dist/ci/sarif.js +61 -0
- package/dist/cli.js +358 -0
- package/dist/commands/ci.js +115 -0
- package/dist/commands/git.js +58 -0
- package/dist/commands/hooks.js +30 -0
- package/dist/commands/hunt-pro.js +1 -0
- package/dist/commands/hunt.js +86 -0
- package/dist/commands/map.js +33 -0
- package/dist/commands/mcp.js +17 -0
- package/dist/commands/pack.js +48 -0
- package/dist/commands/prove-pro.js +1 -0
- package/dist/commands/prove.js +173 -0
- package/dist/commands/report-gate.js +92 -0
- package/dist/commands/scan.js +158 -0
- package/dist/confirm/agreement.js +18 -0
- package/dist/confirm/chain.js +84 -0
- package/dist/confirm/confirm.js +112 -0
- package/dist/confirm/honesty.js +75 -0
- package/dist/confirm/lead.js +92 -0
- package/dist/confirm/merge.js +88 -0
- package/dist/confirm/query.js +145 -0
- package/dist/confirm/rule.js +86 -0
- package/dist/confirm/store.js +42 -0
- package/dist/confirm/summary.js +6 -0
- package/dist/confirm/synthesis.js +28 -0
- package/dist/discover/detectors.js +50 -0
- package/dist/discover/discover.js +270 -0
- package/dist/discover/extract.js +282 -0
- package/dist/discover/git.js +40 -0
- package/dist/discover/kit-files.js +47 -0
- package/dist/discover/walk.js +110 -0
- package/dist/env.js +57 -0
- package/dist/gate/gate.js +173 -0
- package/dist/hooks/install.js +153 -0
- package/dist/hooks/run.js +112 -0
- package/dist/map/analyze.js +201 -0
- package/dist/map/build.js +253 -0
- package/dist/map/candidates.js +49 -0
- package/dist/map/cblocks.js +77 -0
- package/dist/map/config.js +291 -0
- package/dist/map/dotnet.js +242 -0
- package/dist/map/entry.js +78 -0
- package/dist/map/expo.js +190 -0
- package/dist/map/go.js +292 -0
- package/dist/map/js-express.js +143 -0
- package/dist/map/js-misc.js +295 -0
- package/dist/map/js-next.js +292 -0
- package/dist/map/laravel.js +389 -0
- package/dist/map/lex-more.js +305 -0
- package/dist/map/lex.js +425 -0
- package/dist/map/light-index.js +191 -0
- package/dist/map/light-profiles.js +324 -0
- package/dist/map/light.js +574 -0
- package/dist/map/local-vocab.js +27 -0
- package/dist/map/mobile.js +326 -0
- package/dist/map/py-django.js +281 -0
- package/dist/map/py.js +391 -0
- package/dist/map/rails.js +391 -0
- package/dist/map/repo.js +178 -0
- package/dist/map/rust.js +312 -0
- package/dist/map/spring.js +207 -0
- package/dist/map/taint.js +96 -0
- package/dist/map/touches.js +355 -0
- package/dist/map/vocab.js +88 -0
- package/dist/mapping/controls.js +34 -0
- package/dist/mapping/dependency-policy.js +44 -0
- package/dist/mapping/map.js +44 -0
- package/dist/mapping/maps/gitleaks.json +12 -0
- package/dist/mapping/maps/osv-scanner.json +7 -0
- package/dist/mapping/maps/trivy.json +13 -0
- package/dist/mcp/confine.js +59 -0
- package/dist/mcp/protocol.js +140 -0
- package/dist/mcp/schema.js +43 -0
- package/dist/mcp/server.js +37 -0
- package/dist/mcp/tools.js +238 -0
- package/dist/packs/coverage.js +79 -0
- package/dist/packs/manifest.js +36 -0
- package/dist/packs/validate.js +271 -0
- package/dist/prove/container.js +276 -0
- package/dist/prove/exec-main.js +20 -0
- package/dist/prove/executor.js +149 -0
- package/dist/prove/local.js +131 -0
- package/dist/prove/probes.js +65 -0
- package/dist/prove/recipe.js +278 -0
- package/dist/prove/run.js +136 -0
- package/dist/prove/runner.js +1 -0
- package/dist/prove/safety.js +126 -0
- package/dist/prove/spec.js +357 -0
- package/dist/prove/store.js +65 -0
- package/dist/report/build.js +297 -0
- package/dist/report/entry-points.js +75 -0
- package/dist/report/hunt-section.js +71 -0
- package/dist/report/kit.js +41 -0
- package/dist/report/load.js +35 -0
- package/dist/report/messages.js +273 -0
- package/dist/report/prove-section.js +65 -0
- package/dist/report/report.js +156 -0
- package/dist/report/surface.js +21 -0
- package/dist/report/test-code.js +51 -0
- package/dist/report/tiers.js +44 -0
- package/dist/scanners/common.js +162 -0
- package/dist/scanners/gitleaks.js +90 -0
- package/dist/scanners/index.js +113 -0
- package/dist/scanners/osv-scanner.js +77 -0
- package/dist/scanners/run-targets.js +34 -0
- package/dist/scanners/secret-noise.js +89 -0
- package/dist/scanners/semgrep.js +142 -0
- package/dist/scanners/trivy.js +65 -0
- package/dist/scanners/types.js +1 -0
- package/dist/types/check.js +1 -0
- package/dist/types/codemap.js +1 -0
- package/dist/types/findings.js +5 -0
- package/dist/types/hunt.js +1 -0
- package/dist/types/index.js +1 -0
- package/dist/types/prove.js +1 -0
- package/dist/types/results.js +2 -0
- package/dist/types/scan.js +1 -0
- package/dist/types/stack.js +5 -0
- package/dist/util.js +71 -0
- package/dist/version.js +2 -0
- package/package.json +46 -4
- package/packs/android/fixture-result.json +43 -0
- package/packs/android/pack.json +56 -0
- package/packs/dotnet/fixture-result.json +62 -0
- package/packs/dotnet/pack.json +88 -0
- package/packs/expo/answer-key.json +51 -0
- package/packs/expo/fixture-result.json +62 -0
- package/packs/expo/pack.json +79 -0
- package/packs/firebase/answer-key.json +27 -0
- package/packs/firebase/fixture-result.json +43 -0
- package/packs/firebase/pack.json +58 -0
- package/packs/go/answer-key.json +101 -0
- package/packs/go/fixture-result.json +64 -0
- package/packs/go/pack.json +99 -0
- package/packs/ios/fixture-result.json +43 -0
- package/packs/ios/pack.json +55 -0
- package/packs/laravel/answer-key.json +84 -0
- package/packs/laravel/fixture-result.json +60 -0
- package/packs/laravel/pack.json +91 -0
- package/packs/node-ts/answer-key.json +220 -0
- package/packs/node-ts/fixture-result.json +114 -0
- package/packs/node-ts/pack.json +134 -0
- package/packs/python/answer-key.json +195 -0
- package/packs/python/fixture-result.json +119 -0
- package/packs/python/pack.json +121 -0
- package/packs/rails/answer-key.json +84 -0
- package/packs/rails/fixture-result.json +60 -0
- package/packs/rails/pack.json +78 -0
- package/packs/rust/fixture-result.json +43 -0
- package/packs/rust/pack.json +69 -0
- package/packs/spring/fixture-result.json +63 -0
- package/packs/spring/pack.json +103 -0
- package/packs/supabase/answer-key.json +52 -0
- package/packs/supabase/fixture-result.json +58 -0
- package/packs/supabase/pack.json +60 -0
- package/rules/gitleaks.toml +256 -0
- package/rules/semgrep/api-route-no-auth.yaml +31 -0
- package/rules/semgrep/cors-reflect-origin.yaml +48 -0
- package/rules/semgrep/eval-or-new-function.yaml +185 -0
- package/rules/semgrep/log-session-token.yaml +48 -0
- package/rules/semgrep/math-random-tokens.yaml +24 -0
- package/rules/semgrep/next-public-secret-name.yaml +32 -0
- package/rules/semgrep/postgrest-filter-injection.yaml +78 -0
- package/rules/semgrep/raw-sql-concat.yaml +42 -0
- package/rules/semgrep/read-by-id-no-owner.yaml +49 -0
- package/rules/semgrep/server-action-no-auth.yaml +26 -0
- package/rules/semgrep/sql-variants.yaml +99 -0
- package/rules/semgrep/ssrf-unguarded-url.yaml +137 -0
- package/rules/semgrep/subprocess-injection.yaml +163 -0
- package/rules/semgrep/taint-go.yaml +333 -0
- package/rules/semgrep/taint-java.yaml +317 -0
- package/rules/semgrep/taint-javascript.yaml +412 -0
- package/rules/semgrep/taint-php.yaml +361 -0
- package/rules/semgrep/taint-python.yaml +414 -0
- package/rules/semgrep/tool-endpoint-admin-client.yaml +86 -0
- package/rules/semgrep/user-metadata-authz.yaml +22 -0
- package/rules/semgrep/verify-result-unused.yaml +62 -0
- package/rules/semgrep/webhook-parse-before-verify.yaml +29 -0
- package/rules/test-artifact-ids.json +4 -0
- package/rules/tiers.json +427 -0
- package/schemas/check-results.schema.json +55 -0
- package/schemas/check.schema.json +41 -0
- package/schemas/codemap.schema.json +216 -0
- package/schemas/findings.schema.json +1048 -0
- package/schemas/hunt-output.schema.json +46 -0
- package/schemas/hunt-result.schema.json +58 -0
- package/schemas/results.schema.json +17 -0
- package/schemas/scan-hits.schema.json +32 -0
- package/schemas/scan-meta.schema.json +25 -0
- package/schemas/stack.schema.json +93 -0
- package/src/checks/README.md +32 -0
- package/src/checks/code/admin-role.ts +49 -0
- package/src/checks/code/owner-filter.ts +95 -0
- package/src/checks/code/shared.ts +66 -0
- package/src/checks/code/webhook-verify.ts +75 -0
- package/src/checks/context.ts +72 -0
- package/src/checks/map/config-checks.ts +79 -0
- package/src/checks/map/entry-checks.ts +116 -0
- package/src/checks/map/shared.ts +63 -0
- package/src/checks/ops.ts +280 -0
- package/src/checks/pro-map.ts +4 -0
- package/src/checks/run.ts +80 -0
- package/src/checks/types.ts +12 -0
- package/src/ci/comment.ts +52 -0
- package/src/ci/sarif-check.ts +100 -0
- package/src/ci/sarif.ts +72 -0
- package/src/cli.ts +273 -0
- package/src/commands/ci.ts +104 -0
- package/src/commands/git.ts +59 -0
- package/src/commands/hooks.ts +27 -0
- package/src/commands/hunt-pro.ts +4 -0
- package/src/commands/hunt.ts +68 -0
- package/src/commands/map.ts +27 -0
- package/src/commands/mcp.ts +18 -0
- package/src/commands/pack.ts +34 -0
- package/src/commands/prove-pro.ts +4 -0
- package/src/commands/prove.ts +126 -0
- package/src/commands/report-gate.ts +75 -0
- package/src/commands/scan.ts +156 -0
- package/src/confirm/agreement.ts +23 -0
- package/src/confirm/chain.ts +67 -0
- package/src/confirm/confirm.ts +107 -0
- package/src/confirm/honesty.ts +65 -0
- package/src/confirm/lead.ts +76 -0
- package/src/confirm/merge.ts +89 -0
- package/src/confirm/query.ts +126 -0
- package/src/confirm/rule.ts +76 -0
- package/src/confirm/store.ts +42 -0
- package/src/confirm/summary.ts +9 -0
- package/src/confirm/synthesis.ts +33 -0
- package/src/discover/detectors.ts +71 -0
- package/src/discover/discover.ts +253 -0
- package/src/discover/extract.ts +235 -0
- package/src/discover/git.ts +31 -0
- package/src/discover/kit-files.ts +37 -0
- package/src/discover/walk.ts +88 -0
- package/src/env.ts +51 -0
- package/src/gate/gate.ts +169 -0
- package/src/hooks/install.ts +142 -0
- package/src/hooks/run.ts +121 -0
- package/src/map/analyze.ts +219 -0
- package/src/map/build.ts +206 -0
- package/src/map/candidates.ts +47 -0
- package/src/map/cblocks.ts +55 -0
- package/src/map/config.ts +207 -0
- package/src/map/dotnet.ts +205 -0
- package/src/map/entry.ts +80 -0
- package/src/map/expo.ts +171 -0
- package/src/map/go.ts +271 -0
- package/src/map/js-express.ts +135 -0
- package/src/map/js-misc.ts +221 -0
- package/src/map/js-next.ts +282 -0
- package/src/map/laravel.ts +327 -0
- package/src/map/lex-more.ts +152 -0
- package/src/map/lex.ts +274 -0
- package/src/map/light-index.ts +134 -0
- package/src/map/light-profiles.ts +363 -0
- package/src/map/light.ts +484 -0
- package/src/map/local-vocab.ts +30 -0
- package/src/map/mobile.ts +263 -0
- package/src/map/py-django.ts +239 -0
- package/src/map/py.ts +329 -0
- package/src/map/rails.ts +291 -0
- package/src/map/repo.ts +150 -0
- package/src/map/rust.ts +256 -0
- package/src/map/spring.ts +185 -0
- package/src/map/taint.ts +86 -0
- package/src/map/touches.ts +296 -0
- package/src/map/vocab.ts +106 -0
- package/src/mapping/controls.ts +47 -0
- package/src/mapping/dependency-policy.ts +55 -0
- package/src/mapping/map.ts +59 -0
- package/src/mapping/maps/gitleaks.json +12 -0
- package/src/mapping/maps/osv-scanner.json +7 -0
- package/src/mapping/maps/trivy.json +13 -0
- package/src/mcp/confine.ts +44 -0
- package/src/mcp/protocol.ts +144 -0
- package/src/mcp/schema.ts +35 -0
- package/src/mcp/server.ts +32 -0
- package/src/mcp/tools.ts +235 -0
- package/src/packs/coverage.ts +98 -0
- package/src/packs/manifest.ts +97 -0
- package/src/packs/validate.ts +204 -0
- package/src/prove/container.ts +237 -0
- package/src/prove/exec-main.ts +16 -0
- package/src/prove/executor.ts +130 -0
- package/src/prove/local.ts +100 -0
- package/src/prove/probes.ts +68 -0
- package/src/prove/recipe.ts +215 -0
- package/src/prove/run.ts +124 -0
- package/src/prove/runner.ts +28 -0
- package/src/prove/safety.ts +98 -0
- package/src/prove/spec.ts +256 -0
- package/src/prove/store.ts +48 -0
- package/src/report/build.ts +304 -0
- package/src/report/entry-points.ts +78 -0
- package/src/report/hunt-section.ts +67 -0
- package/src/report/kit.ts +59 -0
- package/src/report/load.ts +49 -0
- package/src/report/messages.ts +270 -0
- package/src/report/prove-section.ts +61 -0
- package/src/report/report.ts +160 -0
- package/src/report/surface.ts +32 -0
- package/src/report/test-code.ts +56 -0
- package/src/report/tiers.ts +50 -0
- package/src/scanners/common.ts +149 -0
- package/src/scanners/gitleaks.ts +89 -0
- package/src/scanners/index.ts +103 -0
- package/src/scanners/osv-scanner.ts +90 -0
- package/src/scanners/run-targets.ts +35 -0
- package/src/scanners/secret-noise.ts +80 -0
- package/src/scanners/semgrep.ts +139 -0
- package/src/scanners/trivy.ts +66 -0
- package/src/scanners/types.ts +98 -0
- package/src/types/check.ts +52 -0
- package/src/types/codemap.ts +142 -0
- package/src/types/findings.ts +107 -0
- package/src/types/hunt.ts +97 -0
- package/src/types/index.ts +10 -0
- package/src/types/prove.ts +83 -0
- package/src/types/results.ts +15 -0
- package/src/types/scan.ts +63 -0
- package/src/types/stack.ts +61 -0
- package/src/util.ts +59 -0
- package/src/version.ts +2 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
// hullproof ci: discover, scan, map, check and gate in one command, with no model and no container (hunt is the one option that
|
|
2
|
+
// calls a model, and only in the Pro edition). It writes four files for the commit and exits 0, 1 or 2:
|
|
3
|
+
// findings.json the findings record, schema 1.1.0
|
|
4
|
+
// report.md the plain English report
|
|
5
|
+
// results.sarif SARIF 2.1.0 for code scanning
|
|
6
|
+
// pr-comment.md the summary for a pull request
|
|
7
|
+
// The files are byte identical for the same commit, engine, kit and scanner versions. They go to <out>/<commit>/ and to the flat
|
|
8
|
+
// folder --artifacts (default <out>/ci), so a CI job, an AI tool and a person all read the same paths.
|
|
9
|
+
// Exit 0: nothing at or above --fail-on failed. Exit 1: something did. Exit 2: bad input, a refused run or a records problem.
|
|
10
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import { join, resolve } from 'node:path';
|
|
12
|
+
import { readHeadCommit } from '../discover/git.js';
|
|
13
|
+
import { loadHuntEnv } from '../env.js';
|
|
14
|
+
import { checkSarif } from '../ci/sarif-check.js';
|
|
15
|
+
import { buildSarif, sarifJson } from '../ci/sarif.js';
|
|
16
|
+
import { ciVerdict, FAIL_ON_LEVELS, prComment } from '../ci/comment.js';
|
|
17
|
+
import { gateLines } from '../gate/gate.js';
|
|
18
|
+
import { loadKit, resolveKitDir } from '../report/kit.js';
|
|
19
|
+
import { renderReport } from '../report/report.js';
|
|
20
|
+
import { stableJson } from '../util.js';
|
|
21
|
+
import { huntCommand } from './hunt.js';
|
|
22
|
+
import { HUNT_PRO } from './hunt-pro.js';
|
|
23
|
+
import { prepare } from './report-gate.js';
|
|
24
|
+
import { scan } from './scan.js';
|
|
25
|
+
export const CI_FILES = ['findings.json', 'report.md', 'results.sarif', 'pr-comment.md'];
|
|
26
|
+
export async function ciCommand(o) {
|
|
27
|
+
const log = o.log ?? ((s) => process.stderr.write(s + '\n'));
|
|
28
|
+
const print = o.print ?? ((s) => process.stdout.write(s + '\n'));
|
|
29
|
+
const repo = resolve(o.repo);
|
|
30
|
+
if (!FAIL_ON_LEVELS.includes(o.failOn)) {
|
|
31
|
+
log(`hullproof ci: --fail-on takes ${FAIL_ON_LEVELS.join(', ')}`);
|
|
32
|
+
return 2;
|
|
33
|
+
}
|
|
34
|
+
const kitDir = resolveKitDir(repo, o.kit);
|
|
35
|
+
if (!kitDir) {
|
|
36
|
+
log('hullproof ci: no kit found. Pass --kit <folder holding docs/hullproof/security-controls.json> or set HULLPROOF_KIT.');
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
// The edition follows the kit unless it is named: a kit with Pro requirements runs the full set, the Free kit runs BLOCKER and CRITICAL.
|
|
40
|
+
const edition = o.edition ?? (loadKit(kitDir).controls.some((c) => c.edition === 'pro') ? 'pro' : 'free');
|
|
41
|
+
if (o.hunt) {
|
|
42
|
+
if (!HUNT_PRO) {
|
|
43
|
+
log('hullproof ci: --hunt calls a model and is part of the Pro edition. This edition runs the deterministic checks only.');
|
|
44
|
+
return 2;
|
|
45
|
+
}
|
|
46
|
+
if (edition !== 'pro') {
|
|
47
|
+
log('hullproof ci: --hunt needs the Pro edition requirements (the kit has none, or --edition free was given)');
|
|
48
|
+
return 2;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
const code = await scan({
|
|
52
|
+
repo, out: o.out, kit: kitDir, edition, stage: o.stage, allowNetwork: o.allowNetwork, tools: o.tools, rulePack: o.rulePack, log,
|
|
53
|
+
});
|
|
54
|
+
if (code !== 0)
|
|
55
|
+
return 2;
|
|
56
|
+
if (o.hunt) {
|
|
57
|
+
// The key comes from the environment (a CI secret) or a .env.local file, never from a flag, and is never printed.
|
|
58
|
+
// No .env.local is read in CI: the repository under test is untrusted and must not name the provider address.
|
|
59
|
+
process.env.HULLPROOF_NO_ENV_FILE = '1';
|
|
60
|
+
const { env } = loadHuntEnv(repo);
|
|
61
|
+
if (!o.provider && !env.HULLPROOF_PROVIDER && !env.ANTHROPIC_API_KEY && !env.OPENAI_API_KEY && !env.GEMINI_API_KEY) {
|
|
62
|
+
log('hullproof ci: --hunt needs a provider key in the environment (a CI secret). None is set, so no model was called.');
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
const h = await huntCommand({ repo, out: o.out, kit: kitDir, commit: null, provider: o.provider, model: o.model, runs: null, dryRun: false, resume: false, replay: false, log });
|
|
66
|
+
if (h !== 0)
|
|
67
|
+
return 2;
|
|
68
|
+
}
|
|
69
|
+
const r = await prepare({ repo, out: o.out, kit: kitDir, commit: null, owner: o.owner, date: o.date, log });
|
|
70
|
+
if (typeof r === 'string') {
|
|
71
|
+
log(`hullproof ci: ${r}`);
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
if (r.gate.problems.length) {
|
|
75
|
+
for (const p of r.gate.problems)
|
|
76
|
+
log(`hullproof ci: ${p}`);
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
const findings = r.s.findings;
|
|
80
|
+
const verdict = ciVerdict(findings, o.failOn);
|
|
81
|
+
// The SARIF result of a record with no evidence file points at a file that exists in every repository the engine can scan.
|
|
82
|
+
const anchor = existsSync(join(repo, 'package.json')) ? 'package.json' : existsSync(join(repo, 'README.md')) ? 'README.md' : '.gitignore';
|
|
83
|
+
const sarif = buildSarif(findings, r.kit, anchor);
|
|
84
|
+
const bad = checkSarif(sarif);
|
|
85
|
+
if (bad.length) {
|
|
86
|
+
for (const b of bad)
|
|
87
|
+
log(`hullproof ci: results.sarif: ${b}`);
|
|
88
|
+
return 2;
|
|
89
|
+
}
|
|
90
|
+
const ran = r.s.metas.filter((m) => m.status === 'ran').map((m) => m.tool);
|
|
91
|
+
const files = {
|
|
92
|
+
'findings.json': stableJson(findings),
|
|
93
|
+
'report.md': renderReport(r.s, r.kit, r.gate, r.merged),
|
|
94
|
+
'results.sarif': sarifJson(sarif),
|
|
95
|
+
'pr-comment.md': prComment({ findings, verdict, failOn: o.failOn, edition, scanners: ran }),
|
|
96
|
+
};
|
|
97
|
+
const commit = readHeadCommit(repo);
|
|
98
|
+
const base = resolve(o.out ?? join(repo, '.hullproof'));
|
|
99
|
+
const targets = [join(base, commit), resolve(o.artifacts ?? join(base, 'ci'))];
|
|
100
|
+
for (const t of targets) {
|
|
101
|
+
mkdirSync(t, { recursive: true });
|
|
102
|
+
for (const n of CI_FILES) {
|
|
103
|
+
rmSync(join(t, n), { force: true });
|
|
104
|
+
writeFileSync(join(t, n), files[n]);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
print(`hullproof ci: commit ${commit.slice(0, 12)}, ${edition} edition, stage ${findings.report.stage}, fails on ${o.failOn === 'none' ? 'nothing' : o.failOn.toUpperCase() + ' and above'}`);
|
|
108
|
+
print(`hullproof ci: findings ${verdict.blocking.length + verdict.others.length}, blocking ${verdict.blocking.length}`);
|
|
109
|
+
for (const f of verdict.blocking.slice(0, 20))
|
|
110
|
+
print(` ${f.severity} ${f.sec_ids.join(',')} ${f.evidence[0] ? `${f.evidence[0].file}:${f.evidence[0].line}` : '-'} ${f.title}`);
|
|
111
|
+
print(`hullproof ci: release gate for this commit (informational in CI): ${gateLines(r.gate).at(-1)}`);
|
|
112
|
+
print(`hullproof ci: wrote ${CI_FILES.join(', ')} to ${targets[1]}`);
|
|
113
|
+
print(`hullproof ci: ${verdict.blocked ? 'FAILED, a blocking finding is open' : 'PASSED'}`);
|
|
114
|
+
return verdict.blocked ? 1 : 0;
|
|
115
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Git safety and state. Before any git command (ours or a scanner's), the repository's git configuration is checked
|
|
2
|
+
// with the same three counts as the kit's scan.sh, and the scan is refused when a git command could run code from it.
|
|
3
|
+
import { spawnSync } from 'node:child_process';
|
|
4
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
export function gitSafety(repo) {
|
|
7
|
+
const dotGit = join(repo, '.git');
|
|
8
|
+
if (!existsSync(dotGit))
|
|
9
|
+
return { ok: false, message: `${repo} is not a git repository`, credentialUrl: false };
|
|
10
|
+
let conf;
|
|
11
|
+
let extra = null;
|
|
12
|
+
if (statSync(dotGit).isDirectory()) {
|
|
13
|
+
conf = join(dotGit, 'config');
|
|
14
|
+
// config.worktree (set by newer checkouts, for example actions/checkout) is read with the same three counts as config.
|
|
15
|
+
if (existsSync(join(dotGit, 'config.worktree')))
|
|
16
|
+
extra = join(dotGit, 'config.worktree');
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const m = /^gitdir:\s*(.+)$/m.exec(readFileSync(dotGit, 'utf8'));
|
|
20
|
+
if (!m)
|
|
21
|
+
return { ok: false, message: 'refused: GIT CONFIG: NOT CHECKED (cannot read the gitdir line)', credentialUrl: false };
|
|
22
|
+
conf = join(dirname(dirname(resolve(repo, m[1].trim()))), 'config');
|
|
23
|
+
}
|
|
24
|
+
if (!existsSync(conf))
|
|
25
|
+
return { ok: false, message: 'refused: GIT CONFIG: NOT CHECKED (no config file at the expected place)', credentialUrl: false };
|
|
26
|
+
const lines = [...readFileSync(conf, 'utf8').split('\n'), ...(extra ? readFileSync(extra, 'utf8').split('\n') : [])];
|
|
27
|
+
const c1 = lines.filter((l) => /^[ \t]*(fsmonitor|sshCommand|hooksPath|textconv|pager|editor)[ \t]*=/i.test(l)).length;
|
|
28
|
+
const c2 = lines.filter((l) => /^\[(diff|filter|credential)[ "]|^\[(diff|filter|credential)\]/i.test(l)).length;
|
|
29
|
+
const c3 = lines.filter((l) => /:\/\/[^/ ]*@/.test(l)).length;
|
|
30
|
+
if (c1 > 0 || c2 > 0) {
|
|
31
|
+
return { ok: false, message: `refused: a git command could run code from this folder (code running keys ${c1}, driver and credential sections ${c2}). Remove the entries or scan a fresh clone.`, credentialUrl: c3 > 0 };
|
|
32
|
+
}
|
|
33
|
+
return { ok: true, message: `git config counts: code running keys 0, driver and credential sections 0, remote URLs with credentials ${c3}`, credentialUrl: c3 > 0 };
|
|
34
|
+
}
|
|
35
|
+
// git with the user and system configuration switched off, as scan.sh runs it. Call only after gitSafety passed.
|
|
36
|
+
// HULLPROOF_TRUST_REPO_OWNER=1 is set by the container entry point of the GitHub Action, where the checkout belongs to another user
|
|
37
|
+
// than the one running the engine and git would otherwise refuse it as a dubious ownership folder. It switches that one git check off
|
|
38
|
+
// through a command line style setting, which git honours while the user configuration stays off. The repository's own
|
|
39
|
+
// configuration is still checked by gitSafety above.
|
|
40
|
+
export const TRUST_REPO_OWNER = { GIT_CONFIG_COUNT: '1', GIT_CONFIG_KEY_0: 'safe.directory', GIT_CONFIG_VALUE_0: '*' };
|
|
41
|
+
export const GIT_ENV = {
|
|
42
|
+
GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null', GIT_OPTIONAL_LOCKS: '0', GIT_TERMINAL_PROMPT: '0',
|
|
43
|
+
...(process.env.HULLPROOF_TRUST_REPO_OWNER === '1' ? TRUST_REPO_OWNER : {}),
|
|
44
|
+
};
|
|
45
|
+
function git(repo, args) {
|
|
46
|
+
const r = spawnSync('git', ['-C', repo, ...args], { encoding: 'utf8', env: { ...process.env, ...GIT_ENV } });
|
|
47
|
+
return r.status === 0 ? r.stdout : null;
|
|
48
|
+
}
|
|
49
|
+
// The commit, its tree and whether tracked files differ from it. Untracked files are left out, so engine output written
|
|
50
|
+
// inside the repository does not change the answer between runs.
|
|
51
|
+
export function gitState(repo) {
|
|
52
|
+
const commit = git(repo, ['rev-parse', 'HEAD'])?.trim();
|
|
53
|
+
const tree = git(repo, ['rev-parse', 'HEAD^{tree}'])?.trim();
|
|
54
|
+
const status = git(repo, ['status', '--porcelain', '--untracked-files=no']);
|
|
55
|
+
if (!commit || !tree || status === null || !/^[0-9a-f]{40}$/.test(commit) || !/^[0-9a-f]{40}$/.test(tree))
|
|
56
|
+
return null;
|
|
57
|
+
return { commit, tree, clean: status.trim() === '' };
|
|
58
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// hullproof hooks install | uninstall | run. `run` is what the git hook calls (src/hooks/run.ts).
|
|
2
|
+
import { installHooks } from '../hooks/install.js';
|
|
3
|
+
import { runHook } from '../hooks/run.js';
|
|
4
|
+
export async function hooksCommand(o) {
|
|
5
|
+
const log = o.log ?? ((s) => process.stderr.write(s + '\n'));
|
|
6
|
+
if (o.action === 'install' || o.action === 'uninstall') {
|
|
7
|
+
if (o.mode && !['git', 'husky', 'pre-commit'].includes(o.mode)) {
|
|
8
|
+
log('hullproof hooks: --mode is git, husky or pre-commit');
|
|
9
|
+
return 2;
|
|
10
|
+
}
|
|
11
|
+
const r = installHooks({ repo: o.repo, mode: o.mode, dryRun: o.dryRun, uninstall: o.action === 'uninstall' });
|
|
12
|
+
for (const l of r.lines)
|
|
13
|
+
log(l);
|
|
14
|
+
return r.code;
|
|
15
|
+
}
|
|
16
|
+
if (o.action === 'run') {
|
|
17
|
+
try {
|
|
18
|
+
const r = await runHook({ repo: o.repo, kit: o.kit, semgrep: !o.noSemgrep && process.env.HULLPROOF_HOOK_SEMGREP !== '0' });
|
|
19
|
+
for (const l of r.lines)
|
|
20
|
+
log(l);
|
|
21
|
+
return r.code === 1 ? 1 : r.code === 2 ? 0 : r.code;
|
|
22
|
+
}
|
|
23
|
+
catch (e) {
|
|
24
|
+
log(`hullproof: the pre-commit check could not run (${e.message}). The commit was not blocked.`);
|
|
25
|
+
return process.env.HULLPROOF_HOOK_STRICT === '1' ? 1 : 0;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
log('hullproof hooks: use install, uninstall or run');
|
|
29
|
+
return 2;
|
|
30
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const HUNT_PRO = null;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// hullproof hunt. --replay (open) reruns the confirmation from the saved answers in <out>/<commit>/hunt/ and writes result.json.
|
|
2
|
+
// The other modes call a model through the hunt step that only the Pro edition has (src/commands/hunt-pro.ts).
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { makeContext } from '../checks/context.js';
|
|
5
|
+
import { getMap } from '../checks/map/shared.js';
|
|
6
|
+
import { confirmHunt } from '../confirm/confirm.js';
|
|
7
|
+
import { huntDir, readAnswers, readPlan, readSynthesis, writeResult, writeSynthesis } from '../confirm/store.js';
|
|
8
|
+
import { summary } from '../confirm/summary.js';
|
|
9
|
+
import { reguard } from '../confirm/synthesis.js';
|
|
10
|
+
import { discover } from '../discover/discover.js';
|
|
11
|
+
import { readHeadCommit } from '../discover/git.js';
|
|
12
|
+
import { loadHuntEnv } from '../env.js';
|
|
13
|
+
import { resolveKitDir } from '../report/kit.js';
|
|
14
|
+
import { outDirFor } from './scan.js';
|
|
15
|
+
import { HUNT_PRO } from './hunt-pro.js';
|
|
16
|
+
export const DEFAULT_RUNS = 3;
|
|
17
|
+
export async function huntCommand(o) {
|
|
18
|
+
const log = o.log ?? ((s) => process.stderr.write(s + '\n'));
|
|
19
|
+
const repo = resolve(o.repo);
|
|
20
|
+
const commit = o.commit ?? readHeadCommit(repo);
|
|
21
|
+
if (!commit) {
|
|
22
|
+
log(`hullproof hunt: no commit found in ${repo}; pass --commit`);
|
|
23
|
+
return 2;
|
|
24
|
+
}
|
|
25
|
+
if (o.replay && (o.dryRun || o.resume || o.provider || o.model || o.runs !== null)) {
|
|
26
|
+
log('hullproof hunt: --replay takes no other hunt option');
|
|
27
|
+
return 2;
|
|
28
|
+
}
|
|
29
|
+
if (o.dryRun && o.resume) {
|
|
30
|
+
log('hullproof hunt: --dry-run and --resume cannot be used together');
|
|
31
|
+
return 2;
|
|
32
|
+
}
|
|
33
|
+
const scanDir = outDirFor(repo, o.out, commit);
|
|
34
|
+
const hd = huntDir(scanDir);
|
|
35
|
+
let map;
|
|
36
|
+
let ctx;
|
|
37
|
+
try {
|
|
38
|
+
ctx = makeContext(repo, discover(repo));
|
|
39
|
+
map = getMap(ctx);
|
|
40
|
+
}
|
|
41
|
+
catch (e) {
|
|
42
|
+
log(`hullproof hunt: ${e.message}`);
|
|
43
|
+
return 2;
|
|
44
|
+
}
|
|
45
|
+
if (o.replay) {
|
|
46
|
+
let answers;
|
|
47
|
+
let plan;
|
|
48
|
+
try {
|
|
49
|
+
answers = readAnswers(hd);
|
|
50
|
+
plan = readPlan(hd);
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
log(`hullproof hunt: ${e.message}`);
|
|
54
|
+
return 2;
|
|
55
|
+
}
|
|
56
|
+
if (!answers.length && !plan) {
|
|
57
|
+
log(`hullproof hunt: no saved hunt outputs in ${hd}`);
|
|
58
|
+
return 2;
|
|
59
|
+
}
|
|
60
|
+
const result = confirmHunt({ repo, map, answers, plan, commit });
|
|
61
|
+
writeResult(hd, result);
|
|
62
|
+
const syn = readSynthesis(hd);
|
|
63
|
+
if (syn)
|
|
64
|
+
writeSynthesis(hd, reguard(result, syn));
|
|
65
|
+
for (const l of summary(result))
|
|
66
|
+
log(l);
|
|
67
|
+
log(`hullproof hunt: replay wrote ${hd}/result.json`);
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
70
|
+
if (!HUNT_PRO) {
|
|
71
|
+
log('hullproof hunt: the step that calls a model is not part of this edition. Use --replay to confirm saved outputs.');
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
const runs = o.runs ?? DEFAULT_RUNS;
|
|
75
|
+
if (!Number.isInteger(runs) || runs < 1) {
|
|
76
|
+
log('hullproof hunt: --runs needs a whole number of 1 or more');
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
const kitDir = resolveKitDir(repo, o.kit);
|
|
80
|
+
if (!kitDir) {
|
|
81
|
+
log('hullproof hunt: no kit found. Pass --kit or set HULLPROOF_KIT.');
|
|
82
|
+
return 2;
|
|
83
|
+
}
|
|
84
|
+
const { env } = loadHuntEnv(repo);
|
|
85
|
+
return HUNT_PRO.run({ repo, scanDir, huntDir: hd, commit, map, ctx, kitDir, provider: o.provider, model: o.model, runs, dryRun: o.dryRun, resume: o.resume, env, log });
|
|
86
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// hullproof map: discovery, then the code map. Needs no kit and no scanner. Writes <out>/codemap.json (default: .hullproof
|
|
2
|
+
// inside the repository), or prints it with --out -.
|
|
3
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { join, resolve } from 'node:path';
|
|
5
|
+
import { makeContext } from '../checks/context.js';
|
|
6
|
+
import { getMap } from '../checks/map/shared.js';
|
|
7
|
+
import { discover } from '../discover/discover.js';
|
|
8
|
+
import { stableJson } from '../util.js';
|
|
9
|
+
export function mapCommand(repoArg, outArg, out, err) {
|
|
10
|
+
let json;
|
|
11
|
+
let summary;
|
|
12
|
+
try {
|
|
13
|
+
const repo = resolve(repoArg);
|
|
14
|
+
const ctx = makeContext(repo, discover(repo));
|
|
15
|
+
const map = getMap(ctx);
|
|
16
|
+
json = stableJson(map);
|
|
17
|
+
const notMapped = map.coverage.filter((c) => c.status !== 'mapped');
|
|
18
|
+
summary = `hullproof map: ${map.counts.entry_points} entry points${notMapped.length ? `; map incomplete for ${notMapped.map((c) => c.language).join(', ')}` : ''}`;
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
err(`hullproof map: ${e.message}\n`);
|
|
22
|
+
return 2;
|
|
23
|
+
}
|
|
24
|
+
if (outArg === '-') {
|
|
25
|
+
out(json);
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
const dir = resolve(outArg ?? join(repoArg, '.hullproof'));
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
writeFileSync(join(dir, 'codemap.json'), json);
|
|
31
|
+
out(`${summary}\nhullproof map: wrote ${join(dir, 'codemap.json')}\n`);
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// hullproof mcp: the MCP server over stdio. Read tools always; run_scan only with --allow-run.
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { HullproofTools } from '../mcp/tools.js';
|
|
5
|
+
import { serve } from '../mcp/server.js';
|
|
6
|
+
export async function mcpCommand(o) {
|
|
7
|
+
const log = o.log ?? ((s) => process.stderr.write(s + '\n'));
|
|
8
|
+
const repo = resolve(o.repo);
|
|
9
|
+
if (!existsSync(repo)) {
|
|
10
|
+
log(`hullproof mcp: ${repo} does not exist`);
|
|
11
|
+
return 2;
|
|
12
|
+
}
|
|
13
|
+
const tools = new HullproofTools({ repo, kit: o.kit, edition: o.edition, allowRun: o.allowRun, log });
|
|
14
|
+
log(`hullproof mcp: serving ${repo} on stdio (run_scan ${o.allowRun ? 'on' : 'off'})`);
|
|
15
|
+
await serve(process.stdin, process.stdout, tools);
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// hullproof pack: validate the stack packs (packs/<id>), list them, or print the coverage table made from their recorded results.
|
|
2
|
+
import { existsSync } from 'node:fs';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { coverageMarkdown, coverageRows, legendMarkdown } from '../packs/coverage.js';
|
|
5
|
+
import { LEVEL_NAMES } from '../packs/manifest.js';
|
|
6
|
+
import { defaultEnv, listPacks, PACKS_DIR, validatePack } from '../packs/validate.js';
|
|
7
|
+
export function packCommand(o) {
|
|
8
|
+
const { out, err } = o;
|
|
9
|
+
if (!['validate', 'list', 'table'].includes(o.action)) {
|
|
10
|
+
err(`hullproof pack: use validate, list or table (got "${o.action}").\n`);
|
|
11
|
+
return 2;
|
|
12
|
+
}
|
|
13
|
+
const root = o.root ? resolve(o.root) : undefined;
|
|
14
|
+
if (root && !existsSync(root)) {
|
|
15
|
+
err(`hullproof pack: --root ${root} does not exist.\n`);
|
|
16
|
+
return 2;
|
|
17
|
+
}
|
|
18
|
+
const env = defaultEnv(root);
|
|
19
|
+
const folders = o.targets.length ? o.targets.map((t) => resolve(t)) : listPacks(root ? resolve(root, 'packs') : PACKS_DIR);
|
|
20
|
+
for (const f of folders)
|
|
21
|
+
if (!existsSync(f)) {
|
|
22
|
+
err(`hullproof pack: ${f} does not exist.\n`);
|
|
23
|
+
return 2;
|
|
24
|
+
}
|
|
25
|
+
const checked = folders.map((f) => validatePack(f, env));
|
|
26
|
+
if (o.action === 'table') {
|
|
27
|
+
out(`${legendMarkdown()}\n\n${coverageMarkdown(coverageRows(checked))}\n`);
|
|
28
|
+
return checked.some((c) => c.issues.length) ? 1 : 0;
|
|
29
|
+
}
|
|
30
|
+
if (o.action === 'list') {
|
|
31
|
+
for (const c of checked)
|
|
32
|
+
out(`${c.manifest?.id ?? c.dir}\t${c.manifest ? `${c.manifest.level} ${LEVEL_NAMES[c.manifest.level]}` : 'unreadable'}\t${c.manifest?.name ?? ''}\n`);
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
let failed = 0;
|
|
36
|
+
for (const c of checked) {
|
|
37
|
+
if (!c.issues.length) {
|
|
38
|
+
out(`ok ${c.manifest.id} ${c.manifest.level}${c.skipped.length ? ` (${c.skipped.length} Pro or internal part(s) not checked in this edition)` : ''}\n`);
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
failed++;
|
|
42
|
+
out(`FAIL ${c.manifest?.id ?? c.dir}\n`);
|
|
43
|
+
for (const i of c.issues)
|
|
44
|
+
out(` ${i.code}: ${i.message}\n`);
|
|
45
|
+
}
|
|
46
|
+
out(`hullproof pack validate: ${checked.length - failed} of ${checked.length} packs pass\n`);
|
|
47
|
+
return failed ? 1 : 0;
|
|
48
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PROVE_PRO = null;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
// hullproof prove. Runs test specs against a local copy of the app in a sandbox and keeps the transcripts as evidence. The safety
|
|
2
|
+
// rules are enforced here before anything starts: an explicit flag, the owner's typed confirmation, no target address of any kind,
|
|
3
|
+
// and (inside the runner) no secrets, no network, no host mounts. --replay rebuilds the result from saved transcripts with no
|
|
4
|
+
// sandbox. --gen (Pro) writes specs from hunt leads. --self-test runs the escape attempts on this machine's container runtime.
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
import { createInterface } from 'node:readline/promises';
|
|
7
|
+
import { basename, join, resolve } from 'node:path';
|
|
8
|
+
import { makeContext } from '../checks/context.js';
|
|
9
|
+
import { getMap } from '../checks/map/shared.js';
|
|
10
|
+
import { discover } from '../discover/discover.js';
|
|
11
|
+
import { readHeadCommit } from '../discover/git.js';
|
|
12
|
+
import { loadHuntEnv } from '../env.js';
|
|
13
|
+
import { ContainerRunner, CliRuntime } from '../prove/container.js';
|
|
14
|
+
import { runEscapeProbes, selfTestApp } from '../prove/probes.js';
|
|
15
|
+
import { deriveRecipe } from '../prove/recipe.js';
|
|
16
|
+
import { replayProve, runProve } from '../prove/run.js';
|
|
17
|
+
import { DEFAULT_LIMITS } from '../prove/runner.js';
|
|
18
|
+
import { checkConfirmation, CONFIRM_PHRASE } from '../prove/safety.js';
|
|
19
|
+
import { validateSpec } from '../prove/spec.js';
|
|
20
|
+
import { proveDir, writeSpec } from '../prove/store.js';
|
|
21
|
+
import { huntDir, readResult } from '../confirm/store.js';
|
|
22
|
+
import { outDirFor } from './scan.js';
|
|
23
|
+
import { PROVE_PRO } from './prove-pro.js';
|
|
24
|
+
export function summaryLines(r) {
|
|
25
|
+
const c = r.counts;
|
|
26
|
+
const lines = [`hullproof prove: ${c.proven} proven (3 of 3 runs), ${c.not_proven} not proven, ${c.invalid} invalid, ${c.unstable} unstable, ${c.error} error, ${c.not_provable} not provable here, ${c.dropped} spec file(s) dropped`];
|
|
27
|
+
if (r.not_provable)
|
|
28
|
+
lines.push(`hullproof prove: not provable here (${r.not_provable.code}): ${r.not_provable.reason}`);
|
|
29
|
+
for (const s of r.specs)
|
|
30
|
+
if (s.outcome !== 'proven' && s.outcome !== 'not_provable')
|
|
31
|
+
lines.push(`hullproof prove: ${s.spec_id}: ${s.outcome}: ${s.reason}`);
|
|
32
|
+
if (r.left_behind)
|
|
33
|
+
lines.push(`hullproof prove: WARNING: ${r.left_behind} container, image or network item(s) were left behind`);
|
|
34
|
+
return lines;
|
|
35
|
+
}
|
|
36
|
+
export async function proveCommand(o, deps = {}) {
|
|
37
|
+
const log = o.log ?? ((s) => process.stderr.write(s + '\n'));
|
|
38
|
+
if (o.target !== null) {
|
|
39
|
+
log('hullproof prove: refused. prove runs a local copy of the app in a sandbox and accepts no address. It never sends a request to a staging, production or any other URL.');
|
|
40
|
+
return 2;
|
|
41
|
+
}
|
|
42
|
+
const repo = resolve(o.repo);
|
|
43
|
+
const commit = o.commit ?? readHeadCommit(repo);
|
|
44
|
+
if (!commit) {
|
|
45
|
+
log(`hullproof prove: no commit found in ${repo}; pass --commit`);
|
|
46
|
+
return 2;
|
|
47
|
+
}
|
|
48
|
+
const dir = proveDir(outDirFor(repo, o.out, commit));
|
|
49
|
+
if (o.replay) {
|
|
50
|
+
if (o.gen || o.selfTest || o.specs.length || o.allowRun || o.confirm !== null || o.provider || o.model) {
|
|
51
|
+
log('hullproof prove: --replay takes no other option except --out and --commit');
|
|
52
|
+
return 2;
|
|
53
|
+
}
|
|
54
|
+
const r = replayProve(dir);
|
|
55
|
+
if (typeof r === 'string') {
|
|
56
|
+
log(`hullproof prove: ${r}`);
|
|
57
|
+
return 2;
|
|
58
|
+
}
|
|
59
|
+
for (const l of summaryLines(r))
|
|
60
|
+
log(l);
|
|
61
|
+
log(`hullproof prove: replay wrote ${join(dir, 'result.json')}`);
|
|
62
|
+
return 0;
|
|
63
|
+
}
|
|
64
|
+
// Specs the owner or a person wrote are validated and copied into the prove folder. A bad file is a bad input, named with its reasons.
|
|
65
|
+
for (const f of o.specs) {
|
|
66
|
+
let raw;
|
|
67
|
+
try {
|
|
68
|
+
raw = JSON.parse(readFileSync(resolve(f), 'utf8'));
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
log(`hullproof prove: cannot read spec ${f}: ${e.message}`);
|
|
72
|
+
return 2;
|
|
73
|
+
}
|
|
74
|
+
const v = validateSpec(raw);
|
|
75
|
+
if (!v.ok) {
|
|
76
|
+
log(`hullproof prove: spec ${basename(f)} is invalid: ${v.errors.join('; ')}`);
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
writeSpec(dir, v.spec);
|
|
80
|
+
}
|
|
81
|
+
let stack;
|
|
82
|
+
let map;
|
|
83
|
+
try {
|
|
84
|
+
stack = discover(repo);
|
|
85
|
+
map = getMap(makeContext(repo, stack));
|
|
86
|
+
}
|
|
87
|
+
catch (e) {
|
|
88
|
+
log(`hullproof prove: ${e.message}`);
|
|
89
|
+
return 2;
|
|
90
|
+
}
|
|
91
|
+
if (o.gen) {
|
|
92
|
+
if (!PROVE_PRO) {
|
|
93
|
+
log('hullproof prove: spec generation from leads is a Pro feature and is not part of this edition. Write specs by hand (see docs/INTERFACES.md) and pass them with --spec.');
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
const hunt = readResult(huntDir(outDirFor(repo, o.out, commit)));
|
|
97
|
+
if (!hunt) {
|
|
98
|
+
log('hullproof prove: --gen needs the result of a hunt for this commit. Run hullproof hunt first.');
|
|
99
|
+
return 2;
|
|
100
|
+
}
|
|
101
|
+
const rec = deriveRecipe(repo, stack, map);
|
|
102
|
+
const { env } = loadHuntEnv(repo);
|
|
103
|
+
const code = await PROVE_PRO.generate({ repo, proveDir: dir, hunt, map, recipe: rec.ok ? rec.recipe : null, provider: o.provider, model: o.model, env, log });
|
|
104
|
+
if (code !== 0 || !o.allowRun)
|
|
105
|
+
return code;
|
|
106
|
+
}
|
|
107
|
+
if (!o.allowRun && !o.selfTest) {
|
|
108
|
+
log('hullproof prove: nothing to do. Use --allow-run to run specs, --replay to rebuild the result from saved transcripts, or --gen (Pro) to write specs from leads.');
|
|
109
|
+
return 2;
|
|
110
|
+
}
|
|
111
|
+
// Safety rule 1: the flag and the typed phrase, every run. Asked here, before a runtime is even looked for.
|
|
112
|
+
let typed = o.confirm;
|
|
113
|
+
if (typed === null && (deps.interactive ?? process.stdin.isTTY)) {
|
|
114
|
+
const ask = deps.ask ?? (async (q) => { const rl = createInterface({ input: process.stdin, output: process.stderr }); try {
|
|
115
|
+
return await rl.question(q);
|
|
116
|
+
}
|
|
117
|
+
finally {
|
|
118
|
+
rl.close();
|
|
119
|
+
} });
|
|
120
|
+
log('hullproof prove will build and start a copy of this app in a container, with no secrets, no network and no access to your files. Only the owner of the app may approve that.');
|
|
121
|
+
typed = await ask(`Type exactly "${CONFIRM_PHRASE}" to continue: `);
|
|
122
|
+
}
|
|
123
|
+
const c = checkConfirmation(o.allowRun || o.selfTest, typed);
|
|
124
|
+
if (!c.ok) {
|
|
125
|
+
log(`hullproof prove: ${c.reason}`);
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
const bin = o.runtime ?? process.env.HULLPROOF_CONTAINER_BIN ?? 'docker';
|
|
129
|
+
if (!/^[a-z][a-z0-9._-]{0,30}$/.test(bin)) {
|
|
130
|
+
log('hullproof prove: --runtime names a container command such as docker or podman');
|
|
131
|
+
return 2;
|
|
132
|
+
}
|
|
133
|
+
const runner = deps.runner ?? new ContainerRunner(new CliRuntime(bin));
|
|
134
|
+
const av = await runner.available();
|
|
135
|
+
if (!av.ok) {
|
|
136
|
+
log(`hullproof prove: ${av.reason}`);
|
|
137
|
+
return 2;
|
|
138
|
+
}
|
|
139
|
+
if (o.selfTest)
|
|
140
|
+
return selfTest(runner, log);
|
|
141
|
+
if (o.allowInstallScripts)
|
|
142
|
+
log('hullproof prove: WARNING: --allow-install-scripts is on. Install scripts of every dependency run inside the build container, which can reach the package registry. They run with no secrets and no host mounts, but they are third party code. Use it only for an app you trust.');
|
|
143
|
+
const result = await runProve({ repo, proveDir: dir, commit, stack, map, runner, limits: DEFAULT_LIMITS, allowInstallScripts: o.allowInstallScripts, log });
|
|
144
|
+
for (const l of summaryLines(result))
|
|
145
|
+
log(l);
|
|
146
|
+
log(`hullproof prove: wrote ${join(dir, 'result.json')}`);
|
|
147
|
+
return result.left_behind > 0 || result.counts.error > 0 ? 1 : 0;
|
|
148
|
+
}
|
|
149
|
+
async function selfTest(runner, log) {
|
|
150
|
+
const app = selfTestApp();
|
|
151
|
+
try {
|
|
152
|
+
const p = await runner.prepare({ recipe: app.recipe, copyDir: app.dir, sourceDir: app.dir, limits: DEFAULT_LIMITS });
|
|
153
|
+
if (!p.ok) {
|
|
154
|
+
log(`hullproof prove: self test could not prepare the sandbox: ${p.reason}`);
|
|
155
|
+
return 1;
|
|
156
|
+
}
|
|
157
|
+
const s = await p.prepared.start();
|
|
158
|
+
if (!s.ok) {
|
|
159
|
+
log(`hullproof prove: self test could not start the sandbox: ${s.reason}`);
|
|
160
|
+
await p.prepared.dispose();
|
|
161
|
+
return 1;
|
|
162
|
+
}
|
|
163
|
+
const results = await runEscapeProbes(s.instance, app.marker);
|
|
164
|
+
for (const r of results)
|
|
165
|
+
log(`hullproof prove: self test: ${r.ok ? 'contained' : 'NOT CONTAINED'}: ${r.name} (${r.detail})`);
|
|
166
|
+
const left = (await s.instance.stop()).left_behind.length + (await p.prepared.dispose()).left_behind.length;
|
|
167
|
+
log(`hullproof prove: self test: ${left === 0 ? 'nothing left behind' : `${left} item(s) left behind`}`);
|
|
168
|
+
return results.every((r) => r.ok) && left === 0 ? 0 : 1;
|
|
169
|
+
}
|
|
170
|
+
finally {
|
|
171
|
+
app.cleanup();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// hullproof report and hullproof gate. Both read the scan folder of the current commit (or --commit) and the kit.
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import { readHeadCommit } from '../discover/git.js';
|
|
5
|
+
import { evaluateGate, gateLines, loadKitGate } from '../gate/gate.js';
|
|
6
|
+
import { loadKit, resolveKitDir } from '../report/kit.js';
|
|
7
|
+
import { loadScan } from '../report/load.js';
|
|
8
|
+
import { renderReport } from '../report/report.js';
|
|
9
|
+
import { mergeHunt } from '../confirm/merge.js';
|
|
10
|
+
import { KIT_LOCAL, outDirFor } from './scan.js';
|
|
11
|
+
// The kit folder for report and gate: --kit, then HULLPROOF_KIT, then the kit the scan recorded in kit.local.json when
|
|
12
|
+
// that folder still holds a controls file, then the usual search (the repository, then the development kit).
|
|
13
|
+
export function kitForRead(repo, scanDir, flag) {
|
|
14
|
+
if (flag || process.env.HULLPROOF_KIT)
|
|
15
|
+
return resolveKitDir(repo, flag);
|
|
16
|
+
const p = join(scanDir, KIT_LOCAL);
|
|
17
|
+
if (existsSync(p)) {
|
|
18
|
+
try {
|
|
19
|
+
const rec = JSON.parse(readFileSync(p, 'utf8'));
|
|
20
|
+
if (rec.kit_dir && existsSync(join(rec.kit_dir, 'docs/hullproof/security-controls.json')))
|
|
21
|
+
return rec.kit_dir;
|
|
22
|
+
}
|
|
23
|
+
catch { /* an unreadable record falls through to the usual search */ }
|
|
24
|
+
}
|
|
25
|
+
return resolveKitDir(repo, null);
|
|
26
|
+
}
|
|
27
|
+
const today = () => new Date().toISOString().slice(0, 10);
|
|
28
|
+
export async function prepare(o) {
|
|
29
|
+
const repo = resolve(o.repo);
|
|
30
|
+
const commit = o.commit ?? readHeadCommit(repo);
|
|
31
|
+
if (!commit)
|
|
32
|
+
return `no commit found in ${repo}; pass --commit`;
|
|
33
|
+
const dir = outDirFor(repo, o.out, commit);
|
|
34
|
+
let s;
|
|
35
|
+
try {
|
|
36
|
+
s = loadScan(dir);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
return e.message;
|
|
40
|
+
}
|
|
41
|
+
const kitDir = kitForRead(repo, dir, o.kit);
|
|
42
|
+
if (!kitDir)
|
|
43
|
+
return 'no kit found. Pass --kit or set HULLPROOF_KIT.';
|
|
44
|
+
const kit = loadKit(kitDir);
|
|
45
|
+
if (kit.controlsSha256 !== s.info.kit.controls_sha256)
|
|
46
|
+
return 'the kit controls file differs from the one the scan used. Run hullproof scan again.';
|
|
47
|
+
let owner = null;
|
|
48
|
+
if (o.owner) {
|
|
49
|
+
try {
|
|
50
|
+
owner = JSON.parse(readFileSync(resolve(o.owner), 'utf8'));
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
return `cannot read the owner file: ${e.message}`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (o.date && !/^\d{4}-\d{2}-\d{2}$/.test(o.date))
|
|
57
|
+
return '--date needs YYYY-MM-DD';
|
|
58
|
+
// Confirmed hunt findings and findings proven by a test, and only those, join the rows and the findings record. Leads, unproven specs
|
|
59
|
+
// and runs that were not provable here are never read here.
|
|
60
|
+
const merged = s.hunt.result || s.prove?.result ? mergeHunt(s, kit, s.hunt.result, s.prove?.result ?? null) : null;
|
|
61
|
+
if (merged)
|
|
62
|
+
s = { ...s, results: merged.results, findings: merged.findings, findingsText: merged.findingsText };
|
|
63
|
+
const gate = evaluateGate({
|
|
64
|
+
kitGate: await loadKitGate(kitDir), controls: kit.controls, scope: { edition: s.info.edition, stage: s.info.stage, stageSource: s.info.stage_source },
|
|
65
|
+
rows: s.results, findingsText: s.findingsText, metas: s.metas, owner, repo, commit, today: o.date ?? today(),
|
|
66
|
+
});
|
|
67
|
+
return { s, kit, gate, repo, merged };
|
|
68
|
+
}
|
|
69
|
+
export async function report(o) {
|
|
70
|
+
const log = o.log ?? ((x) => process.stderr.write(x + '\n'));
|
|
71
|
+
const r = await prepare(o);
|
|
72
|
+
if (typeof r === 'string') {
|
|
73
|
+
log(`hullproof report: ${r}`);
|
|
74
|
+
return 2;
|
|
75
|
+
}
|
|
76
|
+
const path = join(r.s.dir, 'report.md');
|
|
77
|
+
writeFileSync(path, renderReport(r.s, r.kit, r.gate, r.merged));
|
|
78
|
+
log(`hullproof report: wrote ${path}`);
|
|
79
|
+
return 0;
|
|
80
|
+
}
|
|
81
|
+
export async function gate(o) {
|
|
82
|
+
const log = o.log ?? ((x) => process.stderr.write(x + '\n'));
|
|
83
|
+
const r = await prepare(o);
|
|
84
|
+
if (typeof r === 'string') {
|
|
85
|
+
log(`hullproof gate: ${r}`);
|
|
86
|
+
return 2;
|
|
87
|
+
}
|
|
88
|
+
for (const l of r.gate.problems)
|
|
89
|
+
log(`hullproof gate: ${l}`);
|
|
90
|
+
process.stdout.write(gateLines(r.gate).join('\n') + '\n');
|
|
91
|
+
return r.gate.exit;
|
|
92
|
+
}
|