@clear-capabilities/agentic-security-scanner 0.143.0 → 0.145.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 +555 -0
- package/bin/agentic-security.js +770 -49
- package/dist/1.index.js +223 -0
- package/dist/113.index.js +117 -18
- package/dist/144.index.js +163 -0
- package/dist/178.index.js +24 -6
- package/dist/238.index.js +3 -2
- package/dist/265.index.js +191 -0
- package/dist/271.index.js +165 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +187 -52
- package/dist/444.index.js +11 -2
- package/dist/449.index.js +76 -12
- package/dist/526.index.js +117 -18
- package/dist/552.index.js +97 -0
- package/dist/637.index.js +27 -5
- package/dist/730.index.js +311 -0
- package/dist/736.index.js +301 -0
- package/dist/824.index.js +7 -0
- package/dist/905.index.js +88 -22
- package/dist/920.index.js +491 -0
- package/dist/970.index.js +173 -0
- package/dist/agentic-security.mjs +13 -13
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/calibration-seed.json +2 -0
- package/package.json +23 -9
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/index.js +18 -0
- package/src/dataflow/privacy-catalog.js +290 -0
- package/src/dataflow/privacy-deep-walker.js +515 -0
- package/src/dataflow/privacy-governance.js +126 -0
- package/src/dataflow/privacy-inventory.js +154 -0
- package/src/dataflow/privacy-sink-policy.js +125 -0
- package/src/dataflow/privacy-taint.js +115 -54
- package/src/dataflow/privacy-taxonomy.js +233 -0
- package/src/discovery/disprove.js +7 -3
- package/src/discovery/hunter.js +9 -5
- package/src/discovery/index.js +2 -2
- package/src/discovery/llm-invoke.js +69 -13
- package/src/egress/audit.js +147 -0
- package/src/egress/policy.js +313 -0
- package/src/egress/redact.js +180 -0
- package/src/engine.js +1048 -302
- package/src/fix/apply-fix-service.js +404 -0
- package/src/fix/approver-registry.js +157 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +1 -1
- package/src/llm-validator/index.js +86 -9
- package/src/llm-validator/model-status.js +66 -0
- package/src/lsp/server.js +49 -2
- package/src/mcp/tools.js +177 -50
- package/src/pipeline/analyzer-supervisor.js +93 -0
- package/src/pipeline/analyzer-worker.js +26 -0
- package/src/pipeline/annotator-runner.js +33 -0
- package/src/pipeline/assurance-mode.js +154 -0
- package/src/pipeline/cascade-worker-pool.js +172 -0
- package/src/pipeline/cascade-worker.js +43 -0
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/detector-runner.js +51 -0
- package/src/pipeline/enrichment-completion.js +58 -0
- package/src/pipeline/evidence-provenance.js +91 -0
- package/src/pipeline/finding-schema.js +108 -0
- package/src/pipeline/legacy-compat.js +101 -0
- package/src/pipeline/producer-collector.js +48 -0
- package/src/pipeline/producer-registry.js +112 -0
- package/src/pipeline/scan-health.js +144 -0
- package/src/posture/CLAUDE.md +123 -0
- package/src/posture/accuracy-scorecard.js +156 -1
- package/src/posture/adversary-agent.js +15 -3
- package/src/posture/artifact-registry.js +241 -0
- package/src/posture/auditor-walkthrough.js +186 -21
- package/src/posture/calibration-feedback.js +201 -0
- package/src/posture/calibration-seed.json +2 -0
- package/src/posture/calibration.js +25 -0
- package/src/posture/compliance-evidence-signing.js +131 -0
- package/src/posture/compliance-policy.js +324 -17
- package/src/posture/cross-repo-memory.js +7 -2
- package/src/posture/custom-rules.js +36 -0
- package/src/posture/deterministic.js +8 -1
- package/src/posture/encryption-provider.js +205 -0
- package/src/posture/evidence-grade-wording.js +71 -0
- package/src/posture/fix-history.js +137 -20
- package/src/posture/fix-honesty-gate.js +47 -6
- package/src/posture/fix-verify.js +65 -8
- package/src/posture/fleet.js +0 -0
- package/src/posture/flow-narration.js +7 -2
- package/src/posture/git-history.js +13 -5
- package/src/posture/legal-hold.js +140 -0
- package/src/posture/llm-redteam.js +10 -1
- package/src/posture/material-change.js +111 -2
- package/src/posture/mttr.js +75 -12
- package/src/posture/policy-bundle.js +274 -0
- package/src/posture/pre-incident-archaeology.js +39 -7
- package/src/posture/privacy-framework.js +47 -6
- package/src/posture/production-feedback.js +179 -0
- package/src/posture/provenance/ai-authorship.js +68 -0
- package/src/posture/provenance/branch-entry.js +80 -0
- package/src/posture/provenance/cache.js +143 -0
- package/src/posture/provenance/confidence.js +36 -0
- package/src/posture/provenance/coordinator.js +786 -0
- package/src/posture/provenance/dag-walk.js +249 -0
- package/src/posture/provenance/evidence-attribution.js +59 -0
- package/src/posture/provenance/git-evidence.js +310 -0
- package/src/posture/provenance/lifecycle.js +208 -0
- package/src/posture/provenance/missing-control-resolver.js +137 -0
- package/src/posture/provenance/origin-resolver.js +342 -0
- package/src/posture/provenance/predicate-replay.js +133 -0
- package/src/posture/provenance/providers/config.js +39 -0
- package/src/posture/provenance/providers/github.js +62 -0
- package/src/posture/provenance/providers/gitlab.js +58 -0
- package/src/posture/provenance/repo-lineage.js +74 -0
- package/src/posture/provenance/sca-origin.js +139 -0
- package/src/posture/provenance/schema.js +255 -0
- package/src/posture/provenance/transitive-sca.js +147 -0
- package/src/posture/provenance/validate.js +30 -0
- package/src/posture/provenance-evidence-bundle.js +144 -0
- package/src/posture/retention-policy.js +132 -0
- package/src/posture/risk-dollars.js +216 -26
- package/src/posture/sbom-diff.js +15 -2
- package/src/posture/scan-checkpoint.js +176 -31
- package/src/posture/secret-history.js +10 -2
- package/src/posture/state-dir.js +64 -5
- package/src/posture/state-lifecycle-report.js +77 -0
- package/src/posture/suppressions.js +59 -3
- package/src/posture/vuln-archaeology.js +8 -2
- package/src/pr-delta.js +25 -4
- package/src/privacy/ir-adapter.js +380 -0
- package/src/report/index.js +248 -5
- package/src/report/oscal.js +7 -2
- package/src/runScan.js +34 -5
- package/src/sast/cpp.js +3 -14
- package/src/sast/rate-limit.js +33 -3
- package/src/sca/llm-function-extract.js +6 -0
- package/src/util/git-hardening.js +128 -0
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// AI-authorship verification hook (Finding Provenance PRD, M4 §4.3).
|
|
2
|
+
//
|
|
3
|
+
// No concrete external signed-commit-metadata standard exists yet to target
|
|
4
|
+
// (the spec's own words) — so this is an extensible REGISTRY a future
|
|
5
|
+
// verifier plugs into, not a hardcoded vendor integration. With nothing
|
|
6
|
+
// registered (today's real state, and likely for some time), every
|
|
7
|
+
// finding's aiAuthorship stays `unknown` — matching the PRD's explicit
|
|
8
|
+
// default: "Unknown unless signed, verifiable generation metadata exists."
|
|
9
|
+
//
|
|
10
|
+
// Scoped to SAST findingOrigin only (see this plan's own scope-correction
|
|
11
|
+
// note) — a transitive/direct SCA origin is a manifest edit, not source
|
|
12
|
+
// authorship in the sense this hook asks about.
|
|
13
|
+
//
|
|
14
|
+
// KNOWN LIMITATION: `commitMeta` (git-evidence.js) is the only shape any
|
|
15
|
+
// verifier registered here ever receives — `{commit, authorName,
|
|
16
|
+
// authorEmail, authorDate, committerDate, summary}`, where `summary` is
|
|
17
|
+
// `%s` (the subject line only), NOT the full commit message body (`%B`).
|
|
18
|
+
// A future verifier that needs to inspect a trailer buried in the body
|
|
19
|
+
// (e.g. a "Co-Authored-By" line that isn't also the subject) cannot see it
|
|
20
|
+
// through this shape today. `commitMeta` is a widely-used shared primitive
|
|
21
|
+
// elsewhere in this directory, so widening it is a bigger, more careful
|
|
22
|
+
// change than this task's budget — deliberately left as a known limitation
|
|
23
|
+
// rather than silently expanding it here.
|
|
24
|
+
|
|
25
|
+
const _verifiers = new Map();
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register a verifier. `verifyFn(commitMeta) -> {status, verifier} | null`.
|
|
29
|
+
* A later registration under the SAME name replaces the earlier one (a
|
|
30
|
+
* re-register, not a stack) — matches this codebase's own precedent
|
|
31
|
+
* elsewhere (verification-separation.js's "one verifier, one vote per lens,
|
|
32
|
+
* a re-vote replaces rather than stuffs").
|
|
33
|
+
*/
|
|
34
|
+
export function registerAIAuthorshipVerifier(name, verifyFn) {
|
|
35
|
+
if (typeof name !== 'string' || !name || typeof verifyFn !== 'function') return false;
|
|
36
|
+
_verifiers.set(name, verifyFn);
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Test/reset helper — never called from production code. */
|
|
41
|
+
export function _clearAIAuthorshipVerifiers() {
|
|
42
|
+
_verifiers.clear();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Consults every registered verifier in registration order; the first one
|
|
47
|
+
* to return a non-null result wins (first-registered-first-consulted, not
|
|
48
|
+
* "last wins" — an explicit choice: a more specific verifier should be
|
|
49
|
+
* registered first if precedence matters, rather than this function
|
|
50
|
+
* guessing which of several opinions to prefer). Defaults to
|
|
51
|
+
* {status:'unknown', verifier:null} with nothing registered or every
|
|
52
|
+
* verifier declining to answer.
|
|
53
|
+
*
|
|
54
|
+
* NEVER THROWS: a verifier that throws is treated as "no opinion", exactly
|
|
55
|
+
* like predicate-replay.js and missing-control-resolver.js already treat a
|
|
56
|
+
* throwing caller-supplied function elsewhere in this directory.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveAIAuthorship(commitMeta) {
|
|
59
|
+
if (!commitMeta) return { status: 'unknown', verifier: null };
|
|
60
|
+
for (const [name, verifyFn] of _verifiers) {
|
|
61
|
+
let result;
|
|
62
|
+
try { result = verifyFn(commitMeta); } catch { continue; }
|
|
63
|
+
if (result && typeof result === 'object' && result.status) {
|
|
64
|
+
return { status: result.status, verifier: result.verifier || name };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return { status: 'unknown', verifier: null };
|
|
68
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Branch-introduction resolver (Finding Provenance PRD, Scenario D).
|
|
2
|
+
//
|
|
3
|
+
// Given a finding's already-resolved origin commit, answers "where did this
|
|
4
|
+
// land on the target branch" — either the origin commit itself (it was
|
|
5
|
+
// committed directly on the branch) or the merge commit that brought it in
|
|
6
|
+
// from a feature branch (`git merge --no-ff`). This is a second, distinct
|
|
7
|
+
// question from origin-resolver.js's "which commit introduced this": a
|
|
8
|
+
// commit can be the true origin and still never have existed on the branch
|
|
9
|
+
// people actually deploy from until a later merge folded it in.
|
|
10
|
+
//
|
|
11
|
+
// Same input-validation posture as git-evidence.js, and for the same reason:
|
|
12
|
+
// `originCommit`/`targetRef` are attacker-influenceable-shaped strings
|
|
13
|
+
// (finding data, CLI args) flowing into `execFileSync('git', [...])`. A
|
|
14
|
+
// value like `--output=/tmp/pwned` must never reach argv as a bare token —
|
|
15
|
+
// see git-evidence.js's SHA_RE/SINCE_RE comments for the incident this
|
|
16
|
+
// guards against (a nominally read-only git call gaining write side effects
|
|
17
|
+
// via flag injection).
|
|
18
|
+
|
|
19
|
+
import * as cp from 'node:child_process';
|
|
20
|
+
import { hardenGitArgs, hardenGitEnv } from '../../util/git-hardening.js';
|
|
21
|
+
|
|
22
|
+
const GIT_TIMEOUT_MS = 2000;
|
|
23
|
+
|
|
24
|
+
// Same hostile-repo hardening as git-evidence.js's `_run`, and for the same
|
|
25
|
+
// reason (FR-PROV-024 / second audit) — this module reimplements its own
|
|
26
|
+
// `_run` rather than importing git-evidence.js's, but that must not mean
|
|
27
|
+
// skipping the config/env hardening every other resolver in this directory
|
|
28
|
+
// gets. `--no-textconv` isn't blanket-applied here: this module's own
|
|
29
|
+
// `_run` calls are `rev-parse`/`merge-base`/`log --format=%H` (no diff/blob
|
|
30
|
+
// CONTENT rendered), so there's no textconv surface to close — see
|
|
31
|
+
// git-evidence.js for the calls where `--no-textconv` is load-bearing.
|
|
32
|
+
function _run(scanRoot, args) {
|
|
33
|
+
try {
|
|
34
|
+
const stdout = cp.execFileSync('git', hardenGitArgs(args), {
|
|
35
|
+
cwd: scanRoot, encoding: 'utf8', timeout: GIT_TIMEOUT_MS,
|
|
36
|
+
stdio: ['ignore', 'pipe', 'ignore'], maxBuffer: 16 * 1024 * 1024,
|
|
37
|
+
env: hardenGitEnv(),
|
|
38
|
+
});
|
|
39
|
+
return { ok: true, stdout };
|
|
40
|
+
} catch (e) {
|
|
41
|
+
return { ok: false, stdout: '' };
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Git SHAs (full or abbreviated) are always lowercase/uppercase hex, 4-40
|
|
46
|
+
// chars. Rejecting anything else closes off flag injection.
|
|
47
|
+
const SHA_RE = /^[0-9a-f]{4,40}$/i;
|
|
48
|
+
function _isSha(sha) {
|
|
49
|
+
return typeof sha === 'string' && SHA_RE.test(sha);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// `targetRef` is used as a revision (both standalone and as the right side
|
|
53
|
+
// of a `originCommit..targetRef` range), so it must look like a ref/branch/
|
|
54
|
+
// sha — never start with `-` (which git would parse as an option) and
|
|
55
|
+
// contain only characters refs can hold.
|
|
56
|
+
const REF_RE = /^[A-Za-z0-9._/-]+$/;
|
|
57
|
+
function _isSafeRef(ref) {
|
|
58
|
+
return typeof ref === 'string' && ref.length > 0 && !ref.startsWith('-') && REF_RE.test(ref);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function _currentBranchName(scanRoot, targetRef) {
|
|
62
|
+
const r = _run(scanRoot, ['rev-parse', '--abbrev-ref', targetRef]);
|
|
63
|
+
return r.ok ? r.stdout.trim() : targetRef;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function resolveBranchEntry(scanRoot, originCommit, targetRef = 'HEAD') {
|
|
67
|
+
if (!_isSha(originCommit)) return null;
|
|
68
|
+
if (!_isSafeRef(targetRef)) return null;
|
|
69
|
+
|
|
70
|
+
const reachable = _run(scanRoot, ['merge-base', '--is-ancestor', originCommit, targetRef]);
|
|
71
|
+
if (!reachable.ok) return null;
|
|
72
|
+
|
|
73
|
+
const branchName = _currentBranchName(scanRoot, targetRef);
|
|
74
|
+
const r = _run(scanRoot, ['log', '--merges', '--ancestry-path', '--reverse', '--format=%H', `${originCommit}..${targetRef}`]);
|
|
75
|
+
if (r.ok && r.stdout.trim()) {
|
|
76
|
+
const firstMerge = r.stdout.trim().split('\n')[0];
|
|
77
|
+
return { commit: firstMerge, ref: `refs/heads/${branchName}`, relationship: 'merge' };
|
|
78
|
+
}
|
|
79
|
+
return { commit: originCommit, ref: `refs/heads/${branchName}`, relationship: 'direct' };
|
|
80
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// Content-addressed provenance cache.
|
|
2
|
+
//
|
|
3
|
+
// Paths and writes both go through posture/state-dir.js rather than joining
|
|
4
|
+
// the state directory name by hand. That seam is what enforces the two
|
|
5
|
+
// invariants this module would otherwise each have to remember: it refuses to
|
|
6
|
+
// create state outside a project root, and it honours the read-only scan
|
|
7
|
+
// switch (`--no-state` / AGENTIC_SECURITY_NO_STATE), so scanning somebody
|
|
8
|
+
// else's tree leaves it byte-identical. See test/no-stray-state.test.js — a
|
|
9
|
+
// cache that quietly writes during a read-only scan is exactly the litter that
|
|
10
|
+
// guard exists to prevent.
|
|
11
|
+
//
|
|
12
|
+
// PRIVACY AT REST (second independent Finding Provenance PRD audit): this
|
|
13
|
+
// cache deliberately stores the FULL provenance record — including raw
|
|
14
|
+
// `authorEmail` — not a pre-redacted one. That is a considered choice, not an
|
|
15
|
+
// oversight: `redactFindingProvenance` is applied per output boundary
|
|
16
|
+
// (report/index.js, mcp/tools.js), and different callers in the SAME scan
|
|
17
|
+
// legitimately want different presentations of the SAME cached record — one
|
|
18
|
+
// reader wants the default-redacted view, another passes
|
|
19
|
+
// `--include-author-email`, a third wants `--pseudonymize-authors`. If the
|
|
20
|
+
// cache stored an already-redacted record, whichever policy was in effect at
|
|
21
|
+
// WRITE time would win for every reader forever, silently breaking that
|
|
22
|
+
// per-call flexibility. So redaction stays a read-time/output-time concern,
|
|
23
|
+
// exactly as documented in posture/CLAUDE.md's "Privacy" section, and this
|
|
24
|
+
// cache is accepted as an at-rest store of raw personal data.
|
|
25
|
+
//
|
|
26
|
+
// The mitigation applied here is a permissions floor, not encryption: every
|
|
27
|
+
// write tightens the `provenance-cache/` directory to 0700 and the entry file
|
|
28
|
+
// to 0600 (same posture this project already uses for the per-install HMAC
|
|
29
|
+
// key at integrity.js's `scan-key`, mode 0600 / 0700 parent). That defeats
|
|
30
|
+
// "any local user/process can read this," which is the realistic at-rest
|
|
31
|
+
// threat for a developer machine or CI runner; it does NOT defeat an attacker
|
|
32
|
+
// with root or the operating-system user's own privileges — no local file
|
|
33
|
+
// permission ever does. Encryption-at-rest with a per-install key (the same
|
|
34
|
+
// pattern as `scan-key`) was considered and rejected for this task's scope:
|
|
35
|
+
// unlike the HMAC key, which only ever needs to reproduce a symmetric digest,
|
|
36
|
+
// a cache that must serve back the exact original record on every read would
|
|
37
|
+
// need the plaintext decrypted on every `cacheGet`, which does not raise the
|
|
38
|
+
// bar much over a permissions floor while adding real complexity (key
|
|
39
|
+
// rotation, corrupt-ciphertext handling) for a cache that is disposable and
|
|
40
|
+
// content-addressed to begin with. If that tradeoff is revisited, encrypting
|
|
41
|
+
// only `findingOrigin.authorEmail` (not the whole record) would preserve this
|
|
42
|
+
// module's byte-identical round-trip property, which is asserted by this
|
|
43
|
+
// file's own tests.
|
|
44
|
+
|
|
45
|
+
import * as fs from 'node:fs';
|
|
46
|
+
import * as path from 'node:path';
|
|
47
|
+
import * as crypto from 'node:crypto';
|
|
48
|
+
import { statePath, safeWriteState } from '../state-dir.js';
|
|
49
|
+
import { FINDING_PROVENANCE_SCHEMA_VERSION } from './schema.js';
|
|
50
|
+
|
|
51
|
+
// Permission floor for the cache directory and every entry inside it — see
|
|
52
|
+
// the module header. Applied on every write, not just at directory creation.
|
|
53
|
+
//
|
|
54
|
+
// Precisely what that does and does not retrofit, because the distinction
|
|
55
|
+
// matters and an earlier version of this comment overstated it: ANY write to
|
|
56
|
+
// this directory retightens the DIRECTORY to 0700, including a write for some
|
|
57
|
+
// unrelated key. An individual ENTRY's own mode is only rewritten when that
|
|
58
|
+
// exact content-addressed key is written again — which a cache hit
|
|
59
|
+
// short-circuits, so in practice a pre-existing 0644 entry keeps that mode.
|
|
60
|
+
// It is still unreachable by another local user, because Unix requires
|
|
61
|
+
// traversal (x) permission on every parent directory to open a file by path,
|
|
62
|
+
// and the 0700 directory denies exactly that. The directory mode is what
|
|
63
|
+
// carries the protection; the file mode is defense in depth.
|
|
64
|
+
const CACHE_DIR_MODE = 0o700;
|
|
65
|
+
const CACHE_FILE_MODE = 0o600;
|
|
66
|
+
|
|
67
|
+
// Its own top-level `.agentic-security/provenance-cache/` directory, NOT
|
|
68
|
+
// nested under `provenance/` (where it lived through M0-M4) — the artifact
|
|
69
|
+
// registry (posture/artifact-registry.js) can only apply retention per
|
|
70
|
+
// TOP-LEVEL directory name, and this cache (pure HEAD-keyed memo, safely
|
|
71
|
+
// regenerable) needs a TTL that the provenance/ lifecycle ledger (permanent
|
|
72
|
+
// history) must never get. See PRD Section 8 / artifact-registry.js's
|
|
73
|
+
// 'provenance-cache' entry. Single helper so the read and write paths can
|
|
74
|
+
// never drift apart.
|
|
75
|
+
function keyPath(scanRoot, key) {
|
|
76
|
+
const hash = crypto.createHash('sha256').update(key).digest('hex');
|
|
77
|
+
return statePath(scanRoot, 'provenance-cache', hash + '.json');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The schema version is part of the key, and is added HERE rather than by the
|
|
82
|
+
* caller so that no caller can forget it.
|
|
83
|
+
*
|
|
84
|
+
* `validate.js` rejects a provenance object stamped with a version this build
|
|
85
|
+
* does not understand — but a cache hit never reached that check: `cacheGet`
|
|
86
|
+
* returns the parsed object as-is. With the version outside the key, entries
|
|
87
|
+
* written by an older schema stayed live key hits after a version bump and
|
|
88
|
+
* flowed straight through, defeating the exact scenario the version field was
|
|
89
|
+
* added to guard. Including it means a bump silently misses every stale entry
|
|
90
|
+
* instead, which is the correct outcome: they are recomputed, not trusted.
|
|
91
|
+
*
|
|
92
|
+
* `lineageKey` (M4 §4.2 final-review fix) covers the resolved
|
|
93
|
+
* `.agentic-security/repo-lineage.json` cross-repo link the same way
|
|
94
|
+
* `historyBoundary` already covers `--provenance-since`: a cross-repo
|
|
95
|
+
* `partial` result IS cacheable, so without this the cache key had no field
|
|
96
|
+
* reflecting which (if any) lineage link produced it. Adding, removing, or
|
|
97
|
+
* repointing the declaration at the same HEAD would then keep serving a
|
|
98
|
+
* stale pre-lineage or a stale cross-repo answer. Callers pass the resolved
|
|
99
|
+
* link's own `${path}@${atCommit}`, or the literal `'none'` when
|
|
100
|
+
* `loadRepoLineage` returns nothing — never omit it in a way that collapses
|
|
101
|
+
* both cases to the same empty string the other fields default to.
|
|
102
|
+
*/
|
|
103
|
+
export function makeCacheKey({ repoHead, stableId, detectorVersion, historyBoundary, mode, lineageKey }) {
|
|
104
|
+
return [
|
|
105
|
+
FINDING_PROVENANCE_SCHEMA_VERSION,
|
|
106
|
+
repoHead || '', stableId || '', detectorVersion || '', historyBoundary || '', mode || '', lineageKey || 'none',
|
|
107
|
+
].join('|');
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function cacheGet(scanRoot, key) {
|
|
111
|
+
// Read directly and let the catch handle "missing" — an explicit
|
|
112
|
+
// existsSync() check first is a check-then-use race for no benefit, since
|
|
113
|
+
// the catch already covers every failure mode a stale check would too.
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(fs.readFileSync(keyPath(scanRoot, key), 'utf8'));
|
|
116
|
+
} catch {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function cacheSet(scanRoot, key, value) {
|
|
122
|
+
try {
|
|
123
|
+
// safeWriteState creates the directory, applies the project-root check and
|
|
124
|
+
// returns false (never throws) when the read-only switch is on. A refused
|
|
125
|
+
// write is a cache miss next time, which is correct behaviour, not an error.
|
|
126
|
+
// category:'provenance-cache' lets lsp/server.js keep THIS write alive
|
|
127
|
+
// while every other state write stays suppressed on every save — see
|
|
128
|
+
// state-dir.js's withStateWritesDisabled.
|
|
129
|
+
const fp = keyPath(scanRoot, key);
|
|
130
|
+
const wrote = safeWriteState(fp, JSON.stringify(value), { category: 'provenance-cache' });
|
|
131
|
+
if (wrote) {
|
|
132
|
+
// Permission floor (see module header) — best-effort, applied AFTER a
|
|
133
|
+
// successful write so a chmod failure (e.g. an unsupported filesystem)
|
|
134
|
+
// never turns a real cache write into a reported failure.
|
|
135
|
+
try {
|
|
136
|
+
fs.chmodSync(path.dirname(fp), CACHE_DIR_MODE);
|
|
137
|
+
fs.chmodSync(fp, CACHE_FILE_MODE);
|
|
138
|
+
} catch { /* best-effort — see above */ }
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
// best-effort — cache failures must never fail a scan
|
|
142
|
+
}
|
|
143
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { CONFIDENCE_LEVEL } from './schema.js';
|
|
2
|
+
|
|
3
|
+
// `renameAmbiguous`: the one production caller (coordinator.js, on the
|
|
4
|
+
// `status:'complete'` path) never passes this true today — see the comment
|
|
5
|
+
// there for why a genuine rename-ambiguous case cannot reach `complete`
|
|
6
|
+
// under the current resolver architecture, so `reasons.push('rename_ambiguous')`
|
|
7
|
+
// below is currently unreachable from real scans. Left wired rather than
|
|
8
|
+
// removed: a caller that DOES have a real rename signal (e.g. a future
|
|
9
|
+
// `complete`-status path that resolves via a rename-following replay) can
|
|
10
|
+
// pass `renameAmbiguous: true` without this function changing at all, and
|
|
11
|
+
// `test/posture/provenance-confidence.test.js` exercises the branch directly.
|
|
12
|
+
export function assessConfidence({
|
|
13
|
+
parentBoundaryVerified = false, historyComplete = false, detectorCompatible = true,
|
|
14
|
+
renameAmbiguous = false, shallow = false, budgetExhausted = false,
|
|
15
|
+
} = {}) {
|
|
16
|
+
const reasons = [];
|
|
17
|
+
if (budgetExhausted) return { level: CONFIDENCE_LEVEL.UNKNOWN, score: 0, reasons: ['budget_exhausted'] };
|
|
18
|
+
|
|
19
|
+
if (parentBoundaryVerified && historyComplete && detectorCompatible && !renameAmbiguous) {
|
|
20
|
+
reasons.push('parent_absence_verified', 'complete_history');
|
|
21
|
+
return { level: CONFIDENCE_LEVEL.HIGH, score: 0.95, reasons };
|
|
22
|
+
}
|
|
23
|
+
if (detectorCompatible && (!historyComplete || !parentBoundaryVerified) && !shallow) {
|
|
24
|
+
if (!historyComplete) reasons.push('partial_history');
|
|
25
|
+
if (!parentBoundaryVerified) reasons.push('no_parent_to_test');
|
|
26
|
+
return { level: CONFIDENCE_LEVEL.MEDIUM, score: 0.65, reasons };
|
|
27
|
+
}
|
|
28
|
+
if (shallow || renameAmbiguous || !detectorCompatible) {
|
|
29
|
+
if (shallow) reasons.push('shallow_history');
|
|
30
|
+
if (renameAmbiguous) reasons.push('rename_ambiguous');
|
|
31
|
+
if (!detectorCompatible) reasons.push('detector_incompatible');
|
|
32
|
+
return { level: CONFIDENCE_LEVEL.LOW, score: 0.35, reasons };
|
|
33
|
+
}
|
|
34
|
+
reasons.push('no_defensible_origin');
|
|
35
|
+
return { level: CONFIDENCE_LEVEL.UNKNOWN, score: 0, reasons };
|
|
36
|
+
}
|