penthera 1.4.0 → 1.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/CHANGELOG.md +57 -0
- package/README.md +141 -33
- package/docs/compliance-cra.md +141 -0
- package/docs/compliance-nis2.md +3 -1
- package/docs/examples/github-actions-scan.yml +1 -1
- package/docs/index.md +2 -2
- package/docs/plan.md +100 -0
- package/lib/compliance/cra.js +525 -0
- package/lib/deps/index.js +32 -8
- package/lib/deps/license.js +329 -0
- package/lib/deps/lockfile.js +4 -1
- package/lib/deps/report.js +17 -0
- package/lib/deps/spdx.json +1 -0
- package/lib/hook/index.js +312 -0
- package/lib/hook/policy.js +62 -0
- package/lib/install/policy.js +10 -2
- package/lib/jurisdictions/nl.json +9 -0
- package/lib/whitebox/aibom.js +384 -0
- package/lib/whitebox/sbom.js +45 -10
- package/lib/whitebox/secrets.js +11 -1
- package/package.json +3 -2
- package/skills/penthera/SKILL.md +33 -4
- package/src/cli/deps.js +22 -2
- package/src/cli/hook.js +131 -0
- package/src/cli.js +64 -6
package/src/cli/deps.js
CHANGED
|
@@ -2,14 +2,32 @@
|
|
|
2
2
|
* `penthera deps [path]`: CLI runner for the dependency capability scan.
|
|
3
3
|
*/
|
|
4
4
|
import { resolve } from "node:path";
|
|
5
|
-
import { existsSync, writeFileSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { existsSync, writeFileSync, mkdirSync, readFileSync } from "node:fs";
|
|
6
6
|
import { dirname } from "node:path";
|
|
7
7
|
import { mergeResults } from "./merge-results.js";
|
|
8
8
|
import { buildActionPlan } from "../../lib/remediation/index.js";
|
|
9
9
|
import { printReport, printFindings, writeJsonReport, writeSarifReport, createProgress, printError } from "../reporter.js";
|
|
10
10
|
import { writeMarkdownReport, markdownPathFromJson } from "../report/markdown.js";
|
|
11
11
|
|
|
12
|
-
export const DEPS_FLAGS = ["diff", "base", "online", "pr-comment", "prod"];
|
|
12
|
+
export const DEPS_FLAGS = ["diff", "base", "online", "pr-comment", "prod", "license-policy"];
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* The license policy for this run: `--license-policy <preset|file>`, else the
|
|
16
|
+
* `licenses` section of penthera.policy.json, else none.
|
|
17
|
+
*/
|
|
18
|
+
async function resolveLicensePolicy(root, flag) {
|
|
19
|
+
const { normalizeLicensePolicy, LICENSE_PRESETS } = await import("../../lib/deps/license.js");
|
|
20
|
+
if (flag) {
|
|
21
|
+
if (LICENSE_PRESETS[flag]) return normalizeLicensePolicy({ preset: flag }, "--license-policy");
|
|
22
|
+
const path = resolve(flag);
|
|
23
|
+
if (!existsSync(path)) throw new Error(`--license-policy: "${flag}" is neither a preset (${Object.keys(LICENSE_PRESETS).join(", ")}) nor a file`);
|
|
24
|
+
let raw;
|
|
25
|
+
try { raw = JSON.parse(readFileSync(path, "utf-8")); } catch (e) { throw new Error(`${flag}: ${e.message}`); }
|
|
26
|
+
return normalizeLicensePolicy(raw && typeof raw === "object" && "licenses" in raw ? raw.licenses : raw, `${flag}: licenses`);
|
|
27
|
+
}
|
|
28
|
+
const { loadPolicy } = await import("../../lib/install/policy.js");
|
|
29
|
+
return loadPolicy(root).policy.licenses;
|
|
30
|
+
}
|
|
13
31
|
|
|
14
32
|
export async function runDeps(args, opts, { version }) {
|
|
15
33
|
if (args.length > 1) {
|
|
@@ -34,7 +52,9 @@ export async function runDeps(args, opts, { version }) {
|
|
|
34
52
|
const { scanDeps } = await import("../../lib/deps/index.js");
|
|
35
53
|
let result;
|
|
36
54
|
try {
|
|
55
|
+
const licensePolicy = await resolveLicensePolicy(root, opts["license-policy"]);
|
|
37
56
|
result = await scanDeps(root, {
|
|
57
|
+
licensePolicy,
|
|
38
58
|
diff: opts.diff || null,
|
|
39
59
|
base: opts.base || null,
|
|
40
60
|
online: !!opts.online,
|
package/src/cli/hook.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `penthera hook install|uninstall|run`: the pre-commit hook (lib/hook/).
|
|
3
|
+
* Exit codes for `run`: 0 commit may go ahead, 1 blocked, 2 error (also blocks).
|
|
4
|
+
*/
|
|
5
|
+
import { parseArgs } from "node:util";
|
|
6
|
+
import { relative, resolve } from "node:path";
|
|
7
|
+
import { runHook, installHook, uninstallHook, fixHint } from "../../lib/hook/index.js";
|
|
8
|
+
import { printError } from "../reporter.js";
|
|
9
|
+
import { bold, dim, green, red, yellow, cyan } from "./ansi.js";
|
|
10
|
+
|
|
11
|
+
const HELP = `
|
|
12
|
+
${bold("penthera hook")} — block secrets, malicious agent config and risky lockfile changes before they are committed
|
|
13
|
+
|
|
14
|
+
${dim("Usage")}
|
|
15
|
+
$ penthera hook install Write .git/hooks/pre-commit (an existing hook is kept and runs first)
|
|
16
|
+
$ penthera hook uninstall Remove it and restore the hook it replaced
|
|
17
|
+
$ penthera hook run Check what is staged now (what the hook runs)
|
|
18
|
+
|
|
19
|
+
${dim("Options")}
|
|
20
|
+
--policy <file> Policy file (default: ./penthera.policy.json)
|
|
21
|
+
--force install: write into core.hooksPath even when another tool manages it
|
|
22
|
+
--json run: print the result as JSON
|
|
23
|
+
-v, --verbose run: also list findings below the blocking severity
|
|
24
|
+
-h, --help Show this help
|
|
25
|
+
|
|
26
|
+
${dim("What it checks (staged content only, offline)")}
|
|
27
|
+
Secrets and AI provider keys in staged files; \`penthera agents\` rules on staged
|
|
28
|
+
.mcp.json, CLAUDE.md, AGENTS.md, .claude/, .cursor/ and skills/; and, when
|
|
29
|
+
package-lock.json is staged, the capability diff against HEAD plus the license
|
|
30
|
+
policy. Blocks on high and critical by default.
|
|
31
|
+
|
|
32
|
+
${dim("Policy")} ${dim("(penthera.policy.json)")}
|
|
33
|
+
{ "hook": { "blockOn": "high", "ignore": ["tests/fixtures/**"],
|
|
34
|
+
"checks": { "secrets": true, "agents": true, "deps": true, "licenses": true } } }
|
|
35
|
+
|
|
36
|
+
${dim("Other hook managers")}
|
|
37
|
+
pre-commit: repo: https://github.com/danoszz/penthera, hooks: [{ id: penthera }]
|
|
38
|
+
husky: echo "npx --no-install penthera hook run" >> .husky/pre-commit
|
|
39
|
+
lefthook: pre-commit: { commands: { penthera: { run: npx --no-install penthera hook run } } }
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
const SEV = { critical: red, high: red, medium: yellow, low: dim, info: dim };
|
|
43
|
+
const where = (f) => `${f.url}${f.line ? `:${f.line}` : ""}`;
|
|
44
|
+
|
|
45
|
+
function findingLine(f, mark) {
|
|
46
|
+
const sev = (SEV[f.severity] || dim)(f.severity.toUpperCase().padEnd(8));
|
|
47
|
+
const ev = f.evidence ? ` ${dim(f.evidence)}` : "";
|
|
48
|
+
return ` ${mark} ${sev} ${cyan(where(f))} ${f.title}${ev} ${dim(`id ${f.id}`)}\n ${dim("→")} ${fixHint(f)}`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function printRun(r, { verbose }) {
|
|
52
|
+
const L = [];
|
|
53
|
+
const secs = (r.duration / 1000).toFixed(1);
|
|
54
|
+
const what = [`${r.staged} staged file${r.staged === 1 ? "" : "s"}`, r.checked.agents && `${r.checked.agents} agent config`, r.checked.lockfiles && `${r.checked.lockfiles} lockfile`].filter(Boolean).join(", ");
|
|
55
|
+
for (const w of r.warnings) L.push(` ${yellow("!")} ${w}`);
|
|
56
|
+
if (!r.blocking.length) {
|
|
57
|
+
const below = r.below.length ? dim(` · ${r.below.length} below ${r.blockOn}${verbose ? "" : " (-v to list)"}`) : "";
|
|
58
|
+
const sup = r.suppressed.length ? dim(` · ${r.suppressed.length} suppressed`) : "";
|
|
59
|
+
L.push(` ${green("✓")} ${bold("penthera")} ${what} checked in ${secs}s${below}${sup}`);
|
|
60
|
+
if (verbose) for (const f of r.below) L.push(findingLine(f, yellow("!")));
|
|
61
|
+
process.stderr.write(L.join("\n") + "\n");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
L.push("");
|
|
65
|
+
L.push(` ${red("✗")} ${bold("penthera")} blocked this commit: ${r.blocking.length} finding${r.blocking.length === 1 ? "" : "s"} at ${r.blockOn} or above ${dim(`(${what}, ${secs}s)`)}`);
|
|
66
|
+
L.push("");
|
|
67
|
+
for (const f of r.blocking) L.push(findingLine(f, red("✗")));
|
|
68
|
+
if (verbose) for (const f of r.below) L.push(findingLine(f, yellow("!")));
|
|
69
|
+
else if (r.below.length) L.push(dim(` + ${r.below.length} below ${r.blockOn}, not blocking (-v to list)`));
|
|
70
|
+
L.push("");
|
|
71
|
+
L.push(` Fix and \`git add\` again. A false positive? Add it to penthera.suppress.json:`);
|
|
72
|
+
L.push(dim(` { "version": 1, "suppress": [{ "id": "${r.blocking[0].id}", "reason": "why this is safe" }] }`));
|
|
73
|
+
L.push(` To skip the check for one commit: ${bold("git commit --no-verify")} ${dim("(the finding still ships)")}`);
|
|
74
|
+
L.push("");
|
|
75
|
+
process.stderr.write(L.join("\n") + "\n");
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function runHookCommand(argv) {
|
|
79
|
+
let parsed;
|
|
80
|
+
try {
|
|
81
|
+
parsed = parseArgs({
|
|
82
|
+
args: argv,
|
|
83
|
+
allowPositionals: true,
|
|
84
|
+
options: {
|
|
85
|
+
policy: { type: "string" },
|
|
86
|
+
force: { type: "boolean", default: false },
|
|
87
|
+
json: { type: "boolean", default: false },
|
|
88
|
+
verbose: { type: "boolean", short: "v", default: false },
|
|
89
|
+
help: { type: "boolean", short: "h", default: false },
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
} catch (e) {
|
|
93
|
+
printError(e.message);
|
|
94
|
+
return 2;
|
|
95
|
+
}
|
|
96
|
+
const { values: opts, positionals } = parsed;
|
|
97
|
+
const sub = positionals[0];
|
|
98
|
+
if (opts.help || !sub) {
|
|
99
|
+
console.log(HELP);
|
|
100
|
+
return sub || opts.help ? 0 : 2;
|
|
101
|
+
}
|
|
102
|
+
const cwd = process.cwd();
|
|
103
|
+
const rel = (p) => relative(cwd, p) || p;
|
|
104
|
+
try {
|
|
105
|
+
if (sub === "install") {
|
|
106
|
+
const r = installHook(cwd, { force: opts.force });
|
|
107
|
+
console.log(` ${green("✓")} ${r.updated ? "Updated" : "Installed"} ${rel(r.path)}`);
|
|
108
|
+
if (r.chained) console.log(` ${dim("▸")} Your existing hook moved to ${rel(r.chained)} and runs first; \`penthera hook uninstall\` puts it back.`);
|
|
109
|
+
console.log(` ${dim("▸")} Every commit now checks staged secrets, agent config and lockfile changes. Skip once with git commit --no-verify.`);
|
|
110
|
+
return 0;
|
|
111
|
+
}
|
|
112
|
+
if (sub === "uninstall") {
|
|
113
|
+
const r = uninstallHook(cwd);
|
|
114
|
+
if (!r.removed) console.log(` ${dim("No penthera pre-commit hook installed.")}`);
|
|
115
|
+
else console.log(` ${green("✓")} Removed ${rel(r.path)}${r.restored ? `; your previous hook is back in place` : ""}`);
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
if (sub === "run") {
|
|
119
|
+
const r = await runHook(cwd, { policyPath: opts.policy ? resolve(opts.policy) : null });
|
|
120
|
+
if (opts.json) console.log(JSON.stringify(r, null, 2));
|
|
121
|
+
else printRun(r, { verbose: opts.verbose });
|
|
122
|
+
return r.blocking.length ? 1 : 0;
|
|
123
|
+
}
|
|
124
|
+
} catch (e) {
|
|
125
|
+
printError(e.message);
|
|
126
|
+
if (sub === "run") process.stderr.write(` ${dim("The commit is blocked because the check could not run. Skip it once with git commit --no-verify.")}\n`);
|
|
127
|
+
return 2;
|
|
128
|
+
}
|
|
129
|
+
printError(`Unknown hook command "${sub}". Use install, uninstall or run.`);
|
|
130
|
+
return 2;
|
|
131
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -24,7 +24,9 @@ import { blankAssessment } from "../lib/compliance/self-assessment.js";
|
|
|
24
24
|
import { buildPolicyPack, policyPackIndex } from "../lib/compliance/policy-pack.js";
|
|
25
25
|
import { parseQuestionnaire, answerQuestionnaire, formatQuestionnaireMarkdown } from "../lib/compliance/questionnaire.js";
|
|
26
26
|
import { buildIncidentReports, formatIncidentMarkdown, incidentTemplate } from "../lib/compliance/incident.js";
|
|
27
|
-
import { buildSbom } from "../lib/whitebox/sbom.js";
|
|
27
|
+
import { buildSbom, buildAiBom } from "../lib/whitebox/sbom.js";
|
|
28
|
+
import { aiBomSummary } from "../lib/whitebox/aibom.js";
|
|
29
|
+
import { buildCraReports, craTemplate, formatCraMarkdown, readCraInput } from "../lib/compliance/cra.js";
|
|
28
30
|
import { emptyHistory, updateHistory, buildAuditLoop } from "../lib/audit-loop.js";
|
|
29
31
|
import { rateSuppliers, parseSupplierList, formatSuppliersMarkdown } from "../lib/supply-chain.js";
|
|
30
32
|
|
|
@@ -89,10 +91,14 @@ const HELP = `
|
|
|
89
91
|
--online Also check npm registry metadata (the only network use)
|
|
90
92
|
--prod Skip devDependencies
|
|
91
93
|
--pr-comment <file> Write a short Markdown capability diff for a PR comment
|
|
94
|
+
--license-policy <p> permissive | weak-copyleft-ok | no-agpl, or a policy file
|
|
92
95
|
|
|
93
96
|
${dim("Install mode")} ${dim("(penthera install [pkg...]; penthera install --help for all options)")}
|
|
94
97
|
--yes / --dry-run Install only if nothing breaches the policy / report only
|
|
95
98
|
|
|
99
|
+
${dim("Hook mode")} ${dim("(penthera hook install|uninstall|run; penthera hook --help)")}
|
|
100
|
+
Pre-commit: staged secrets, agent config, lockfile diff
|
|
101
|
+
|
|
96
102
|
${dim("Options")}
|
|
97
103
|
-r, --repo <path> Path to repo for source analysis
|
|
98
104
|
--api-root <path> API routes directory (default: auto-detect)
|
|
@@ -114,6 +120,9 @@ const HELP = `
|
|
|
114
120
|
--incident <file> Generate NIS2 incident-report drafts + timeline
|
|
115
121
|
--incident-init <file> Write a blank incident template and exit
|
|
116
122
|
--sbom <file> Write a CycloneDX software bill of materials (from --repo)
|
|
123
|
+
--aibom <file> Write a CycloneDX 1.6 AI-BOM: AI SDKs, models, MCP servers, skills, prompts
|
|
124
|
+
--cra <file> Generate CRA Article 14 report drafts + timeline (writes cra-reports.md)
|
|
125
|
+
--cra-init <file> Write a blank CRA reporting template and exit
|
|
117
126
|
--history <file> Track findings over time (time-to-fix, ageing, drift)
|
|
118
127
|
--memory Learn from this scan locally; apply suppressions + ranking
|
|
119
128
|
--suppliers <file> Passively rate suppliers' public domains (RAG, measure d)
|
|
@@ -166,6 +175,12 @@ export async function run() {
|
|
|
166
175
|
return;
|
|
167
176
|
}
|
|
168
177
|
|
|
178
|
+
if (process.argv[2] === "hook") {
|
|
179
|
+
const { runHookCommand } = await import("./cli/hook.js");
|
|
180
|
+
process.exitCode = await runHookCommand(process.argv.slice(3));
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
169
184
|
if (process.argv[2] === "evidence") {
|
|
170
185
|
const { runEvidence } = await import("./cli/evidence.js");
|
|
171
186
|
process.exit(runEvidence(process.argv.slice(3)));
|
|
@@ -206,6 +221,9 @@ export async function run() {
|
|
|
206
221
|
incident: { type: "string" },
|
|
207
222
|
"incident-init": { type: "string" },
|
|
208
223
|
sbom: { type: "string" },
|
|
224
|
+
aibom: { type: "string" },
|
|
225
|
+
cra: { type: "string" },
|
|
226
|
+
"cra-init": { type: "string" },
|
|
209
227
|
history: { type: "string" },
|
|
210
228
|
memory: { type: "boolean", default: false },
|
|
211
229
|
suppliers: { type: "string" },
|
|
@@ -214,6 +232,7 @@ export async function run() {
|
|
|
214
232
|
online: { type: "boolean", default: false },
|
|
215
233
|
"pr-comment": { type: "string" },
|
|
216
234
|
prod: { type: "boolean", default: false },
|
|
235
|
+
"license-policy": { type: "string" },
|
|
217
236
|
profile: { type: "string" },
|
|
218
237
|
"auth-cookie": { type: "string" },
|
|
219
238
|
"auth-bearer": { type: "string" },
|
|
@@ -292,6 +311,32 @@ export async function run() {
|
|
|
292
311
|
process.exit(0);
|
|
293
312
|
}
|
|
294
313
|
|
|
314
|
+
if (opts["cra-init"]) {
|
|
315
|
+
const path = resolve(opts["cra-init"]);
|
|
316
|
+
writeFileSync(path, JSON.stringify(craTemplate(), null, 2) + "\n");
|
|
317
|
+
console.log(`Wrote a blank CRA reporting template to ${path}`);
|
|
318
|
+
console.log(`Set type and aware_at (ISO 8601), then run: penthera --cra ${opts["cra-init"]}`);
|
|
319
|
+
process.exit(0);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
if (opts.cra) {
|
|
323
|
+
let input;
|
|
324
|
+
try {
|
|
325
|
+
input = readCraInput(resolve(opts.cra));
|
|
326
|
+
} catch (e) {
|
|
327
|
+
printError(`Could not read --cra file: ${e.message}`);
|
|
328
|
+
process.exit(2);
|
|
329
|
+
}
|
|
330
|
+
for (const err of input.errors) printError(err);
|
|
331
|
+
const result = buildCraReports(input.cra, loadJurisdiction(), { now: new Date().toISOString(), prefilled: input.prefilled });
|
|
332
|
+
const outPath = resolve("cra-reports.md");
|
|
333
|
+
writeFileSync(outPath, formatCraMarkdown(result));
|
|
334
|
+
console.log(`CRA Article 14 report drafts written to ${outPath}`);
|
|
335
|
+
for (const t of result.timeline) console.log(` ${t.title} (${t.within}): ${t.dueBy ? `due ${t.dueBy}` : t.basis || "n/a"}, ${t.status}`);
|
|
336
|
+
for (const w of result.warnings) console.log(` ⚠️ ${w}`);
|
|
337
|
+
process.exit(0);
|
|
338
|
+
}
|
|
339
|
+
|
|
295
340
|
if (opts.suppliers && opts.offline) {
|
|
296
341
|
printError("--suppliers rates third-party domains, which --offline forbids.");
|
|
297
342
|
process.exit(2);
|
|
@@ -326,8 +371,8 @@ export async function run() {
|
|
|
326
371
|
await runDeps(positionals.slice(1), opts, { version: pkg.version });
|
|
327
372
|
return;
|
|
328
373
|
}
|
|
329
|
-
if (opts.diff || opts.base || opts.online || opts["pr-comment"] || opts.prod) {
|
|
330
|
-
printError("--diff, --base, --online, --prod
|
|
374
|
+
if (opts.diff || opts.base || opts.online || opts["pr-comment"] || opts.prod || opts["license-policy"]) {
|
|
375
|
+
printError("--diff, --base, --online, --prod, --pr-comment and --license-policy only apply to `penthera deps`.");
|
|
331
376
|
process.exit(2);
|
|
332
377
|
}
|
|
333
378
|
|
|
@@ -352,8 +397,8 @@ export async function run() {
|
|
|
352
397
|
const url = positionals[0] && !agentsMode ? normalizeBaseUrl(positionals[0]) : null;
|
|
353
398
|
const repo = opts.repo || null;
|
|
354
399
|
|
|
355
|
-
if (!agentsMode && !url && !repo && !opts.machine && !opts.readiness && !opts["policy-pack"] && !opts.sbom && !opts.questionnaire) {
|
|
356
|
-
printError("No target specified. Provide a URL, --repo, --machine, --readiness, --policy-pack, --sbom, --questionnaire, or combine them.");
|
|
400
|
+
if (!agentsMode && !url && !repo && !opts.machine && !opts.readiness && !opts["policy-pack"] && !opts.sbom && !opts.aibom && !opts.questionnaire) {
|
|
401
|
+
printError("No target specified. Provide a URL, --repo, --machine, --readiness, --policy-pack, --sbom, --aibom, --questionnaire, or combine them.");
|
|
357
402
|
console.log(HELP);
|
|
358
403
|
process.exit(2);
|
|
359
404
|
}
|
|
@@ -609,6 +654,19 @@ export async function run() {
|
|
|
609
654
|
}
|
|
610
655
|
}
|
|
611
656
|
|
|
657
|
+
if (opts.aibom) {
|
|
658
|
+
const aibom = buildAiBom(resolve(opts.repo || "."), {
|
|
659
|
+
serialNumber: `urn:uuid:${randomUUID()}`,
|
|
660
|
+
timestamp: new Date().toISOString(),
|
|
661
|
+
toolVersion: pkg.version,
|
|
662
|
+
});
|
|
663
|
+
writeFileSync(resolve(opts.aibom), JSON.stringify(aibom, null, 2) + "\n");
|
|
664
|
+
if (!opts.quiet && !opts.json) {
|
|
665
|
+
const n = aiBomSummary(aibom);
|
|
666
|
+
process.stderr.write(` AI-BOM (CycloneDX 1.6) ${n.sdks} SDKs, ${n.models} models, ${n.mcpServers} MCP servers, ${n.skills} skills, ${n.prompts} prompts → ${opts.aibom}\n`);
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
612
670
|
if (opts["policy-pack"]) {
|
|
613
671
|
const dir = resolve(opts["policy-pack"]);
|
|
614
672
|
mkdirSync(dir, { recursive: true });
|
|
@@ -668,7 +726,7 @@ export async function run() {
|
|
|
668
726
|
);
|
|
669
727
|
}
|
|
670
728
|
|
|
671
|
-
if ((opts.output || opts.markdown || opts.sarif || opts.evidence || opts.readiness || opts["policy-pack"] || opts.sbom || opts.history || opts.questionnaire) && !opts.quiet && !opts.json) {
|
|
729
|
+
if ((opts.output || opts.markdown || opts.sarif || opts.evidence || opts.readiness || opts["policy-pack"] || opts.sbom || opts.aibom || opts.history || opts.questionnaire) && !opts.quiet && !opts.json) {
|
|
672
730
|
process.stderr.write("\n");
|
|
673
731
|
}
|
|
674
732
|
|