@quantakrypto/qscan 0.4.4 → 0.6.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/README.md +37 -9
- package/dist/args.d.ts +69 -9
- package/dist/args.d.ts.map +1 -1
- package/dist/args.js +123 -5
- package/dist/args.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +98 -10
- package/dist/cli.js.map +1 -1
- package/dist/color.d.ts +38 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +44 -0
- package/dist/color.js.map +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +41 -1
- package/dist/config.js.map +1 -1
- package/dist/help.d.ts +1 -1
- package/dist/help.d.ts.map +1 -1
- package/dist/help.js +50 -2
- package/dist/help.js.map +1 -1
- package/dist/index.d.ts +60 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +178 -23
- package/dist/index.js.map +1 -1
- package/dist/remediate-cli.d.ts +3 -4
- package/dist/remediate-cli.d.ts.map +1 -1
- package/dist/remediate-cli.js +1 -1
- package/dist/remediate-cli.js.map +1 -1
- package/dist/report.d.ts +14 -2
- package/dist/report.d.ts.map +1 -1
- package/dist/report.js +61 -6
- package/dist/report.js.map +1 -1
- package/dist/sign.d.ts +4 -0
- package/dist/sign.d.ts.map +1 -0
- package/dist/sign.js +61 -0
- package/dist/sign.js.map +1 -0
- package/dist/triage-run.d.ts +3 -4
- package/dist/triage-run.d.ts.map +1 -1
- package/dist/triage-run.js +13 -6
- package/dist/triage-run.js.map +1 -1
- package/package.json +3 -3
package/dist/triage-run.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"triage-run.js","sourceRoot":"","sources":["../src/triage-run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `qscan --triage` glue (BYOK plane). Runs the LLM triage pass over a scan\n * result and attaches an exposure annotation to each finding, then re-sorts by\n * exposure. It NEVER drops a finding and NEVER touches the exit code — the CLI\n * computes the exit code from raw severities before this ever runs.\n *\n * `@quantakrypto/agent` (the only networked package) is loaded via dynamic\n * `import()` so a plain scan never pulls in the network client.\n */\nimport { readFile as fsReadFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport process from \"node:process\";\n\nimport {\n buildContext,\n compareFindings,\n fingerprintFinding,\n renderPreflight,\n} from \"@quantakrypto/core\";\nimport type {\n ContextLevel,\n Finding,\n ScanResult,\n Severity,\n TriageVerdict,\n} from \"@quantakrypto/core\";\n\nimport type { LlmProvider } from \"./args.js\";\n\nconst SEVERITY_RANK: Record<Severity, number> = {\n critical: 0,\n high: 1,\n medium: 2,\n low: 3,\n info: 4,\n};\n\n/** Default cap on findings sent to the LLM per triage run (spend/DoS guard). */\nexport const DEFAULT_MAX_TRIAGE = 100;\n\n/** The model's `rationale` is untrusted text that lands in JSON/SARIF output.\n * Strip control characters and clamp length so a prompt-injected rationale can't\n * smuggle escape sequences or unbounded content into a downstream consumer. */\nfunction sanitizeRationale(s: string): string {\n // eslint-disable-next-line no-control-regex\n const clean = s.replace(/[\\u0000-\\u001f\\u007f]+/g, \" \").trim();\n return clean.length > 500 ? `${clean.slice(0, 497)}…` : clean;\n}\n\n/** Injectable triage function (default wraps `@quantakrypto/agent`). */\nexport type TriageFn = (findings: readonly Finding[]) => Promise<Map<string, TriageVerdict>>;\n\nexport interface RunTriageOptions {\n level: ContextLevel;\n floor?: Severity;\n dryRun?: boolean;\n provider?: LlmProvider;\n model?: string;\n cacheFile?: string;\n /** Cap on findings sent to the LLM (spend/DoS guard; default DEFAULT_MAX_TRIAGE). */\n maxFindings?: number;\n /** Base directory for resolving finding file paths (defaults to result.root). */\n root?: string;\n // --- injectables for testing ---\n triageFn?: TriageFn;\n resolveKey?: () => string | undefined;\n readFile?: (relPath: string) => Promise<string>;\n stderr?: (s: string) => void;\n}\n\nexport interface RunTriageResult {\n /** When `--dry-run`, the preflight text to show instead of a normal report. */\n preflight?: string;\n}\n\nfunction envKey(provider: LlmProvider): string | undefined {\n return (\n process.env.QK_LLM_API_KEY ??\n (provider === \"anthropic\" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY)\n );\n}\n\nfunction defaultModel(provider: LlmProvider): string {\n return provider === \"anthropic\" ? \"claude-sonnet-5\" : \"gpt-4o-mini\";\n}\n\n/**\n * Annotate `result.findings` with triage verdicts (mutating `result`). Returns a\n * preflight string when `--dry-run` is set (no provider is contacted). Failures\n * degrade gracefully: the scan/report proceed without triage.\n */\nexport async function runTriage(\n result: ScanResult,\n opts: RunTriageOptions,\n): Promise<RunTriageResult> {\n const level = opts.level;\n const floorRank = SEVERITY_RANK[opts.floor ?? \"medium\"];\n const targets = result.findings.filter((f) => SEVERITY_RANK[f.severity] <= floorRank);\n const stderr = opts.stderr ?? ((s: string) => void process.stderr.write(s));\n const root = opts.root ?? result.root ?? \".\";\n const readFile = opts.readFile ?? ((rel: string) => fsReadFile(path.resolve(root, rel), \"utf8\"));\n\n // --dry-run: show exactly what would be sent; contact nothing.\n if (opts.dryRun) {\n const contexts = [];\n for (const f of targets) {\n const content = level === \"metadata\" ? \"\" : await readFile(f.location.file).catch(() => \"\");\n contexts.push(buildContext(f, level, content));\n }\n return {\n preflight: contexts.length\n ? renderPreflight(contexts)\n : \"qscan --triage --dry-run: no findings at or above the triage floor.\",\n };\n }\n\n const provider: LlmProvider = opts.provider ?? \"anthropic\";\n const key = opts.resolveKey ? opts.resolveKey() : envKey(provider);\n\n // No key and no injected triage function → graceful degrade (never fatal).\n if (!opts.triageFn && !key) {\n stderr(\n \"qscan: --triage needs an API key (set QK_LLM_API_KEY, ANTHROPIC_API_KEY, or OPENAI_API_KEY). Skipping triage.\\n\",\n );\n return {};\n }\n\n const model = opts.model ?? defaultModel(provider);\n const triageFn: TriageFn =\n opts.triageFn ??\n (async (findings) => {\n const agent = await import(\"@quantakrypto/agent\");\n const client = agent.resolveClient({ provider, model, apiKey: key as string });\n return agent.triageFindings(findings, {\n client,\n level,\n readFile,\n fingerprint: fingerprintFinding,\n floor: opts.floor,\n cacheFile: opts.cacheFile,\n model,\n });\n });\n\n // Spend/DoS guard: triage only up to maxFindings (top by severity among those\n // at/above the floor). The rest keep their deterministic order and no annotation.\n const maxFindings = opts.maxFindings ?? DEFAULT_MAX_TRIAGE;\n const toTriage =\n targets.length > maxFindings\n ? [...targets].sort(compareFindings).slice(0, maxFindings)\n : result.findings;\n if (targets.length > maxFindings) {\n stderr(\n `qscan: --triage capped at ${maxFindings} findings (${targets.length} at/above floor); raise --max-findings to triage more.\\n`,\n );\n }\n\n try {\n const verdicts = await triageFn(toTriage);\n for (const f of result.findings) {\n const v = verdicts.get(fingerprintFinding(f));\n if (v) {\n f.triage = {\n exposureScore: v.exposureScore,\n priority: v.priority,\n rationale: sanitizeRationale(v.rationale),\n };\n }\n }\n // Re-sort by exposure (desc), falling back to the stable finding order.\n result.findings = [...result.findings].sort((a, b) => {\n const ea = a.triage?.exposureScore ?? -1;\n const eb = b.triage?.exposureScore ?? -1;\n if (eb !== ea) return eb - ea;\n return compareFindings(a, b);\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n stderr(`qscan: triage failed (${msg}); showing findings without triage.\\n`);\n }\n return {};\n}\n"]}
|
|
1
|
+
{"version":3,"file":"triage-run.js","sourceRoot":"","sources":["../src/triage-run.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * `qscan --triage` glue (BYOK plane). Runs the LLM triage pass over a scan\n * result and attaches an exposure annotation to each finding, then re-sorts by\n * exposure. It NEVER drops a finding and NEVER touches the exit code — the CLI\n * computes the exit code from raw severities before this ever runs.\n *\n * `@quantakrypto/agent` (the only networked package) is loaded via dynamic\n * `import()` so a plain scan never pulls in the network client.\n */\nimport { readFile as fsReadFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport process from \"node:process\";\n\nimport {\n buildContext,\n compareFindings,\n fingerprintFinding,\n renderPreflight,\n severityRank,\n} from \"@quantakrypto/core\";\nimport type {\n ContextLevel,\n Finding,\n ScanResult,\n Severity,\n TriageVerdict,\n} from \"@quantakrypto/core\";\n\nimport type { LlmProvider } from \"./args.js\";\n\nconst SEVERITY_RANK: Record<Severity, number> = {\n critical: 0,\n high: 1,\n medium: 2,\n low: 3,\n info: 4,\n};\n\n/** Default cap on findings sent to the LLM per triage run (spend/DoS guard). */\nconst DEFAULT_MAX_TRIAGE = 100;\n\n/** The model's `rationale` is untrusted text that lands in JSON/SARIF output.\n * Strip control characters and clamp length so a prompt-injected rationale can't\n * smuggle escape sequences or unbounded content into a downstream consumer. */\nfunction sanitizeRationale(s: string): string {\n // eslint-disable-next-line no-control-regex\n const clean = s.replace(/[\\u0000-\\u001f\\u007f]+/g, \" \").trim();\n return clean.length > 500 ? `${clean.slice(0, 497)}…` : clean;\n}\n\n/** Injectable triage function (default wraps `@quantakrypto/agent`). */\nexport type TriageFn = (findings: readonly Finding[]) => Promise<Map<string, TriageVerdict>>;\n\ninterface RunTriageOptions {\n level: ContextLevel;\n floor?: Severity;\n dryRun?: boolean;\n provider?: LlmProvider;\n model?: string;\n cacheFile?: string;\n /** Cap on findings sent to the LLM (spend/DoS guard; default DEFAULT_MAX_TRIAGE). */\n maxFindings?: number;\n /** Base directory for resolving finding file paths (defaults to result.root). */\n root?: string;\n // --- injectables for testing ---\n triageFn?: TriageFn;\n resolveKey?: () => string | undefined;\n readFile?: (relPath: string) => Promise<string>;\n stderr?: (s: string) => void;\n}\n\ninterface RunTriageResult {\n /** When `--dry-run`, the preflight text to show instead of a normal report. */\n preflight?: string;\n}\n\nfunction envKey(provider: LlmProvider): string | undefined {\n return (\n process.env.QK_LLM_API_KEY ??\n (provider === \"anthropic\" ? process.env.ANTHROPIC_API_KEY : process.env.OPENAI_API_KEY)\n );\n}\n\nfunction defaultModel(provider: LlmProvider): string {\n return provider === \"anthropic\" ? \"claude-sonnet-5\" : \"gpt-4o-mini\";\n}\n\n/**\n * Annotate `result.findings` with triage verdicts (mutating `result`). Returns a\n * preflight string when `--dry-run` is set (no provider is contacted). Failures\n * degrade gracefully: the scan/report proceed without triage.\n */\nexport async function runTriage(\n result: ScanResult,\n opts: RunTriageOptions,\n): Promise<RunTriageResult> {\n const level = opts.level;\n const floorRank = SEVERITY_RANK[opts.floor ?? \"medium\"];\n const targets = result.findings.filter((f) => SEVERITY_RANK[f.severity] <= floorRank);\n const stderr = opts.stderr ?? ((s: string) => void process.stderr.write(s));\n const root = opts.root ?? result.root ?? \".\";\n const readFile = opts.readFile ?? ((rel: string) => fsReadFile(path.resolve(root, rel), \"utf8\"));\n\n // --dry-run: show exactly what would be sent; contact nothing.\n if (opts.dryRun) {\n const contexts = [];\n for (const f of targets) {\n const content = level === \"metadata\" ? \"\" : await readFile(f.location.file).catch(() => \"\");\n contexts.push(buildContext(f, level, content));\n }\n return {\n preflight: contexts.length\n ? renderPreflight(contexts)\n : \"qscan --triage --dry-run: no findings at or above the triage floor.\",\n };\n }\n\n const provider: LlmProvider = opts.provider ?? \"anthropic\";\n const key = opts.resolveKey ? opts.resolveKey() : envKey(provider);\n\n // No key and no injected triage function → graceful degrade (never fatal).\n if (!opts.triageFn && !key) {\n stderr(\n \"qscan: --triage needs an API key (set QK_LLM_API_KEY, ANTHROPIC_API_KEY, or OPENAI_API_KEY). Skipping triage.\\n\",\n );\n return {};\n }\n\n const model = opts.model ?? defaultModel(provider);\n const triageFn: TriageFn =\n opts.triageFn ??\n (async (findings) => {\n const agent = await import(\"@quantakrypto/agent\");\n const client = agent.resolveClient({ provider, model, apiKey: key as string });\n return agent.triageFindings(findings, {\n client,\n level,\n readFile,\n fingerprint: fingerprintFinding,\n floor: opts.floor,\n cacheFile: opts.cacheFile,\n model,\n });\n });\n\n // Spend/DoS guard: triage only up to maxFindings, and when capping, pick the TOP by\n // SEVERITY (most severe first) — not by file-path order — so a critical in a\n // late-sorting file isn't silently dropped from triage and sunk to the bottom of the\n // report. Under the cap, triage exactly the `targets` (findings at/above the floor),\n // not the full result set. Ties fall back to the stable file/line/ruleId order.\n const maxFindings = opts.maxFindings ?? DEFAULT_MAX_TRIAGE;\n const bySeverityThenOrder = (a: Finding, b: Finding): number => {\n const d = severityRank(a.severity) - severityRank(b.severity);\n return d !== 0 ? d : compareFindings(a, b);\n };\n const toTriage =\n targets.length > maxFindings\n ? [...targets].sort(bySeverityThenOrder).slice(0, maxFindings)\n : targets;\n if (targets.length > maxFindings) {\n stderr(\n `qscan: --triage capped at ${maxFindings} findings (${targets.length} at/above floor); raise --max-findings to triage more.\\n`,\n );\n }\n\n try {\n const verdicts = await triageFn(toTriage);\n for (const f of result.findings) {\n const v = verdicts.get(fingerprintFinding(f));\n if (v) {\n f.triage = {\n exposureScore: v.exposureScore,\n priority: v.priority,\n rationale: sanitizeRationale(v.rationale),\n };\n }\n }\n // Re-sort by exposure (desc), falling back to the stable finding order.\n result.findings = [...result.findings].sort((a, b) => {\n const ea = a.triage?.exposureScore ?? -1;\n const eb = b.triage?.exposureScore ?? -1;\n if (eb !== ea) return eb - ea;\n return compareFindings(a, b);\n });\n } catch (err) {\n const msg = err instanceof Error ? err.message : String(err);\n stderr(`qscan: triage failed (${msg}); showing findings without triage.\\n`);\n }\n return {};\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quantakrypto/qscan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "qScan — find quantum-vulnerable cryptography in any codebase (CLI). Zero runtime dependencies.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Dandelion Labs <hello@dandelionlabs.io> (https://dandelionlabs.io)",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"access": "public"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@quantakrypto/agent": "0.
|
|
41
|
-
"@quantakrypto/core": "0.
|
|
40
|
+
"@quantakrypto/agent": "0.6.0",
|
|
41
|
+
"@quantakrypto/core": "0.6.0"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "tsc -b",
|