@enwithai/product-check 0.1.0 → 0.3.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/ATTRIBUTION.md +6 -6
- package/CHANGELOG.md +66 -0
- package/NOTICE.md +5 -3
- package/README.md +34 -19
- package/SECURITY.md +5 -1
- package/dashboard/assurance.css +71 -0
- package/dashboard/assurance.js +21 -0
- package/dashboard/dashboard.css +6 -0
- package/dist/assurance/adapters.d.ts +32 -0
- package/dist/assurance/adapters.js +76 -0
- package/dist/assurance/approvals.d.ts +34 -0
- package/dist/assurance/approvals.js +133 -0
- package/dist/assurance/cli.d.ts +13 -0
- package/dist/assurance/cli.js +863 -0
- package/dist/assurance/dashboard.d.ts +18 -0
- package/dist/assurance/dashboard.js +267 -0
- package/dist/assurance/diff.d.ts +20 -0
- package/dist/assurance/diff.js +99 -0
- package/dist/assurance/evidence-log.d.ts +20 -0
- package/dist/assurance/evidence-log.js +100 -0
- package/dist/assurance/fix-packets.d.ts +12 -0
- package/dist/assurance/fix-packets.js +178 -0
- package/dist/assurance/gate.d.ts +37 -0
- package/dist/assurance/gate.js +324 -0
- package/dist/assurance/lanes.d.ts +61 -0
- package/dist/assurance/lanes.js +380 -0
- package/dist/assurance/policy.d.ts +10 -0
- package/dist/assurance/policy.js +85 -0
- package/dist/assurance/proof.d.ts +35 -0
- package/dist/assurance/proof.js +146 -0
- package/dist/assurance/register.d.ts +41 -0
- package/dist/assurance/register.js +480 -0
- package/dist/assurance/render-md.d.ts +17 -0
- package/dist/assurance/render-md.js +212 -0
- package/dist/assurance/repo-state.d.ts +15 -0
- package/dist/assurance/repo-state.js +79 -0
- package/dist/assurance/review.d.ts +52 -0
- package/dist/assurance/review.js +408 -0
- package/dist/assurance/risk.d.ts +21 -0
- package/dist/assurance/risk.js +184 -0
- package/dist/assurance/types.d.ts +573 -0
- package/dist/assurance/types.js +6 -0
- package/dist/assurance/util.d.ts +24 -0
- package/dist/assurance/util.js +89 -0
- package/dist/assurance/verification.d.ts +30 -0
- package/dist/assurance/verification.js +212 -0
- package/dist/assurance/verify.d.ts +26 -0
- package/dist/assurance/verify.js +304 -0
- package/dist/blueprint/build.d.ts +3 -1
- package/dist/blueprint/build.js +41 -5
- package/dist/cli/run.js +103 -5
- package/dist/contract/contract.d.ts +4 -0
- package/dist/contract/contract.js +16 -0
- package/dist/dashboard/render.d.ts +5 -0
- package/dist/dashboard/render.js +107 -3
- package/dist/discovery/pops.d.ts +2 -0
- package/dist/discovery/pops.js +7 -5
- package/dist/exec/process.d.ts +27 -0
- package/dist/exec/process.js +175 -0
- package/dist/importers/operational.d.ts +56 -0
- package/dist/importers/operational.js +217 -0
- package/dist/importers/reports.d.ts +24 -0
- package/dist/importers/reports.js +37 -1
- package/dist/inspectors/source/surface.d.ts +30 -0
- package/dist/inspectors/source/surface.js +529 -0
- package/dist/parse/yaml.d.ts +4 -0
- package/dist/parse/yaml.js +39 -0
- package/dist/reporting/markdown.d.ts +1 -0
- package/dist/reporting/markdown.js +18 -1
- package/dist/reporting/spec-markdown.d.ts +9 -0
- package/dist/reporting/spec-markdown.js +144 -0
- package/dist/run.d.ts +2 -0
- package/dist/run.js +52 -2
- package/dist/schema/registry.js +10 -0
- package/dist/scoring/score.js +1 -1
- package/dist/spec/build.d.ts +28 -0
- package/dist/spec/build.js +278 -0
- package/dist/types.d.ts +78 -2
- package/dist/types.js +6 -1
- package/dist/validation/catalog.d.ts +1 -1
- package/dist/validation/catalog.js +125 -2
- package/dist/validation/context.d.ts +6 -1
- package/dist/validation/import-evaluators.js +37 -3
- package/dist/validation/integrity.js +4 -0
- package/dist/validation/operational-evaluators.d.ts +3 -0
- package/dist/validation/operational-evaluators.js +153 -0
- package/dist/validation/source-evaluators.js +1 -1
- package/package.json +4 -1
- package/schemas/approval.schema.json +98 -0
- package/schemas/as-built-spec.schema.json +356 -0
- package/schemas/assurance-common.schema.json +291 -0
- package/schemas/assurance-policy.schema.json +185 -0
- package/schemas/blueprint.schema.json +3 -0
- package/schemas/change-review.schema.json +711 -0
- package/schemas/closure-register.schema.json +499 -0
- package/schemas/evidence-record.schema.json +94 -0
- package/schemas/fix-packet.schema.json +272 -0
- package/schemas/release-gate.schema.json +308 -0
- package/schemas/reviewer-findings.schema.json +49 -0
- package/schemas/scan-contract.schema.json +20 -0
- package/schemas/scorecard.schema.json +1 -1
- package/schemas/verification-run.schema.json +199 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { findingRisk } from "./register.js";
|
|
2
|
+
import { ASSURANCE_SCHEMA_VERSION, METHODOLOGY_VERSION, } from "./types.js";
|
|
3
|
+
const ELIGIBLE = new Set(["CONFIRMED", "CORROBORATED", "REPRODUCED"]);
|
|
4
|
+
const ACTIVE = new Set([
|
|
5
|
+
"OPEN",
|
|
6
|
+
"ACKNOWLEDGED",
|
|
7
|
+
"IN_PROGRESS",
|
|
8
|
+
"FIX_PROPOSED",
|
|
9
|
+
"RETEST_REQUIRED",
|
|
10
|
+
"REGRESSED",
|
|
11
|
+
]);
|
|
12
|
+
const STANDARD_CONSTRAINTS = [
|
|
13
|
+
"Change only what the packet's scope guard allows; anything else needs a new review.",
|
|
14
|
+
"Do not disable, skip or weaken tests, checks or security controls to make verification pass.",
|
|
15
|
+
"Use synthetic data; never commit credentials or real personal data.",
|
|
16
|
+
"A proposed fix is not a verified fix: run product-check verify against the exact fix commit.",
|
|
17
|
+
"An agent cannot approve its own change; closure needs signed human approval for the risk tier.",
|
|
18
|
+
];
|
|
19
|
+
const NEGATIVE = {
|
|
20
|
+
trust: [
|
|
21
|
+
"An unauthenticated request to the affected path is refused (401/403), not served.",
|
|
22
|
+
"A signed-in user cannot read or change another user's or tenant's record by changing an ID.",
|
|
23
|
+
"No credential or privileged value appears in the client bundle or responses.",
|
|
24
|
+
],
|
|
25
|
+
technology: [
|
|
26
|
+
"Error, empty and timeout paths return a handled response, not a crash or silent fallback.",
|
|
27
|
+
"Retrying the operation does not repeat its side effect.",
|
|
28
|
+
],
|
|
29
|
+
customer: ["Empty, error and loading states are understandable and keyboard-accessible."],
|
|
30
|
+
};
|
|
31
|
+
/** Packets for active, verified P0–P2 entries of the register. */
|
|
32
|
+
export function buildFixPackets(reg, results, reviews, policy) {
|
|
33
|
+
const packets = [];
|
|
34
|
+
const policyTests = policy.required_checks.map((c) => ({
|
|
35
|
+
id: `regression:${c.id}`,
|
|
36
|
+
kind: "regression",
|
|
37
|
+
description: `Policy required check ${c.id} (${c.purpose}).`,
|
|
38
|
+
argv: c.argv,
|
|
39
|
+
}));
|
|
40
|
+
for (const e of reg.entries) {
|
|
41
|
+
if (!ACTIVE.has(e.state) || !ELIGIBLE.has(e.verification.state) || e.severity === "P3")
|
|
42
|
+
continue;
|
|
43
|
+
packets.push(e.source === "baseline-scan"
|
|
44
|
+
? fromResults(e, results, policyTests, policy)
|
|
45
|
+
: fromReview(e, reviews, policyTests, policy));
|
|
46
|
+
}
|
|
47
|
+
return packets;
|
|
48
|
+
}
|
|
49
|
+
function base(e, policy) {
|
|
50
|
+
return {
|
|
51
|
+
schema_version: ASSURANCE_SCHEMA_VERSION,
|
|
52
|
+
kind: "fix-packet",
|
|
53
|
+
methodology_version: METHODOLOGY_VERSION,
|
|
54
|
+
fix_id: `FP-${e.finding_id}`,
|
|
55
|
+
finding_ids: [e.finding_id],
|
|
56
|
+
source_finding_ids: [e.source_finding_id],
|
|
57
|
+
status: e.state === "RETEST_REQUIRED" || e.state === "FIX_PROPOSED"
|
|
58
|
+
? "IMPLEMENTED_UNVERIFIED"
|
|
59
|
+
: e.state === "REGRESSED"
|
|
60
|
+
? "REGRESSED"
|
|
61
|
+
: "PROPOSED",
|
|
62
|
+
risk_class: findingRisk(e.severity),
|
|
63
|
+
severity: e.severity,
|
|
64
|
+
owner: { id: null, accountable_human_required: true },
|
|
65
|
+
implementation: { change_sha: null, implemented_by: null },
|
|
66
|
+
rollback_plan: `Revert the fix commit (git revert <fix-sha>) and redeploy the previous release${policy.release.rollback_plan ? `; follow ${policy.release.rollback_plan}` : ""}. If the fix includes a data migration, run its down step first and confirm with a backup restore test.`,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function fromResults(e, results, policyTests, policy) {
|
|
70
|
+
const f = results?.findings.find((x) => x.id === e.source_finding_id);
|
|
71
|
+
const rem = results?.blueprint.remediations.find((r) => r.finding_ids.includes(e.source_finding_id));
|
|
72
|
+
const wp = results?.blueprint.work_packets.find((w) => w.finding_ids.includes(e.source_finding_id));
|
|
73
|
+
const trace = results?.blueprint.traceability.find((t) => t.finding_id === e.source_finding_id);
|
|
74
|
+
const ev = (f?.evidence_ids ?? [])
|
|
75
|
+
.map((id) => results?.evidence.find((x) => x.id === id))
|
|
76
|
+
.filter((x) => x !== undefined);
|
|
77
|
+
const surfaces = ev.length
|
|
78
|
+
? ev.map((x) => x.kind === "http-response"
|
|
79
|
+
? { path: null, line: null, runtime: x.source.locator }
|
|
80
|
+
: { path: x.source.locator, line: x.source.line_start, runtime: null })
|
|
81
|
+
: [{ path: e.location, line: null, runtime: null }];
|
|
82
|
+
const files = [...new Set(surfaces.map((s) => s.path).filter((p) => p !== null))];
|
|
83
|
+
return {
|
|
84
|
+
...base(e, policy),
|
|
85
|
+
title: `${e.rule_or_control}: ${e.title}`,
|
|
86
|
+
affected_surfaces: surfaces,
|
|
87
|
+
user_impact: [f?.impact.customer, f?.impact.trust, f?.impact.technology]
|
|
88
|
+
.filter((x) => typeof x === "string")
|
|
89
|
+
.join(" ") || "Not stated in the results.",
|
|
90
|
+
evidence_refs: f?.evidence_ids ?? [],
|
|
91
|
+
root_cause: {
|
|
92
|
+
hypothesis: f?.root_cause.statement ?? "Not stated.",
|
|
93
|
+
certainty: f?.root_cause.certainty ?? "hypothesis",
|
|
94
|
+
confidence: f?.confidence ?? "LOW",
|
|
95
|
+
},
|
|
96
|
+
constraints: [...new Set([...(wp?.constraints ?? []), ...STANDARD_CONSTRAINTS])],
|
|
97
|
+
proposed_change: rem?.recommended_change ?? "See the control's remediation guidance.",
|
|
98
|
+
tests_required: [
|
|
99
|
+
{
|
|
100
|
+
id: `reproduction:${e.finding_id}`,
|
|
101
|
+
kind: "reproduction",
|
|
102
|
+
description: `Product Check re-assesses ${e.rule_or_control} at the fix commit (built into verify). Also declare a test that fails before the fix and passes after it (fix --repro-cmd).`,
|
|
103
|
+
argv: null,
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: `regression:${e.finding_id}`,
|
|
107
|
+
kind: "regression",
|
|
108
|
+
description: rem?.regression_test.description ?? "A regression test for this behavior in CI.",
|
|
109
|
+
argv: null,
|
|
110
|
+
},
|
|
111
|
+
...policyTests,
|
|
112
|
+
],
|
|
113
|
+
negative_cases: NEGATIVE[e.pillar] ?? [],
|
|
114
|
+
dependencies: rem?.depends_on ?? [],
|
|
115
|
+
acceptance_conditions: [
|
|
116
|
+
...(rem?.acceptance_criteria ?? []),
|
|
117
|
+
`product-check verify --finding ${e.finding_id} passes at the exact fix commit (targeted + regression).`,
|
|
118
|
+
`Signed human approval(s) for closure (${policy.risk.approvals[findingRisk(e.severity)]} for ${findingRisk(e.severity)} risk).`,
|
|
119
|
+
],
|
|
120
|
+
scope_guard: {
|
|
121
|
+
allowed_paths: [...files, "test/**", "tests/**", "**/*.test.*", "**/*.spec.*"],
|
|
122
|
+
note: "Edits outside these paths need explicit approval or a new scan contract (scope drift).",
|
|
123
|
+
},
|
|
124
|
+
traceability: {
|
|
125
|
+
control_or_rule: e.rule_or_control,
|
|
126
|
+
spec_requirement_ids: trace
|
|
127
|
+
? [trace.journey_or_requirement.split(":")[0] ?? trace.journey_or_requirement]
|
|
128
|
+
: [],
|
|
129
|
+
locations: surfaces.map((s) => s.runtime ?? `${s.path}${s.line !== null ? `:${s.line}` : ""}`),
|
|
130
|
+
},
|
|
131
|
+
generated_from: { results_run_id: results?.run.run_id ?? null, review_id: null },
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
function fromReview(e, reviews, policyTests, policy) {
|
|
135
|
+
const review = reviews.find((r) => r.findings.some((f) => f.id === e.source_finding_id));
|
|
136
|
+
const f = review?.findings.find((x) => x.id === e.source_finding_id);
|
|
137
|
+
const path = f?.location.path ?? e.location;
|
|
138
|
+
return {
|
|
139
|
+
...base(e, policy),
|
|
140
|
+
title: `${e.rule_or_control}: ${e.title}`,
|
|
141
|
+
affected_surfaces: [{ path, line: f?.location.line ?? null, runtime: f?.location.runtime ?? null }],
|
|
142
|
+
user_impact: f?.impact ?? "Not stated.",
|
|
143
|
+
evidence_refs: f?.evidence_ids ?? [],
|
|
144
|
+
root_cause: {
|
|
145
|
+
hypothesis: f ? `${f.actual} Expected: ${f.expected}` : e.title,
|
|
146
|
+
certainty: e.epistemic,
|
|
147
|
+
confidence: f?.confidence ?? "LOW",
|
|
148
|
+
},
|
|
149
|
+
constraints: STANDARD_CONSTRAINTS,
|
|
150
|
+
proposed_change: f?.expected ?? "Restore the expected behavior.",
|
|
151
|
+
tests_required: [
|
|
152
|
+
{
|
|
153
|
+
id: `reproduction:${e.finding_id}`,
|
|
154
|
+
kind: "reproduction",
|
|
155
|
+
description: `The ${e.rule_or_control} rule no longer matches at the fix commit (built into verify), plus a declared test that fails before the fix.`,
|
|
156
|
+
argv: null,
|
|
157
|
+
},
|
|
158
|
+
...policyTests,
|
|
159
|
+
],
|
|
160
|
+
negative_cases: NEGATIVE[e.pillar] ?? [],
|
|
161
|
+
dependencies: [],
|
|
162
|
+
acceptance_conditions: [
|
|
163
|
+
f?.expected ?? "Expected behavior restored.",
|
|
164
|
+
`product-check verify --finding ${e.finding_id} passes at the exact fix commit.`,
|
|
165
|
+
`Signed human approval(s) for closure (${policy.risk.approvals[findingRisk(e.severity)]}).`,
|
|
166
|
+
],
|
|
167
|
+
scope_guard: {
|
|
168
|
+
allowed_paths: [path, "test/**", "tests/**", "**/*.test.*", "**/*.spec.*"],
|
|
169
|
+
note: "Edits outside these paths need explicit approval or a new review.",
|
|
170
|
+
},
|
|
171
|
+
traceability: {
|
|
172
|
+
control_or_rule: e.rule_or_control,
|
|
173
|
+
spec_requirement_ids: [],
|
|
174
|
+
locations: [`${path}${f?.location.line ? `:${f.location.line}` : ""}`],
|
|
175
|
+
},
|
|
176
|
+
generated_from: { results_run_id: null, review_id: review?.review_id ?? null },
|
|
177
|
+
};
|
|
178
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release gate (VC-P3/VC-P6): GO / CONDITIONAL_GO / NO_GO / UNKNOWN from the product verdict, the closure
|
|
3
|
+
* register, the change review, author-side proof and signed approvals. Rules, in order:
|
|
4
|
+
*
|
|
5
|
+
* NO_GO a substantiated blocker: open verified P0/P1, failed hard gate or required check, pending retest,
|
|
6
|
+
* regression, a NOT_READY product verdict or a BLOCK change gate.
|
|
7
|
+
* UNKNOWN nothing was executed (no passing executed check of any kind), the product verdict is
|
|
8
|
+
* INCONCLUSIVE, coverage is below the contract minimum, or required checks/approvals cannot be
|
|
9
|
+
* evaluated. A zero-finding scan with no executed checks is UNKNOWN, never green.
|
|
10
|
+
* NO_GO (then) release preconditions unmet with sufficient evidence: approvals for HEAD, rollback plan.
|
|
11
|
+
* CONDITIONAL_GO accepted risks, open P2s, a READY_WITH_CONDITIONS product verdict, a CONDITIONAL change gate, or
|
|
12
|
+
* a release condition that could not be evaluated.
|
|
13
|
+
* GO every condition PASSES.
|
|
14
|
+
* A high score never overrides a blocker.
|
|
15
|
+
*/
|
|
16
|
+
import type { Results } from "../types.js";
|
|
17
|
+
import { type Approval, type AssurancePolicy, type ChangeReview, type ClosureRegister, type FixPacket, type PhaseProgress, type ReleaseGate, type VerificationRun } from "./types.js";
|
|
18
|
+
export interface GateInput {
|
|
19
|
+
results: Results;
|
|
20
|
+
resultsSha: string;
|
|
21
|
+
register: ClosureRegister | null;
|
|
22
|
+
review: ChangeReview | null;
|
|
23
|
+
proof: VerificationRun | null;
|
|
24
|
+
packets: FixPacket[];
|
|
25
|
+
approvals: Approval[];
|
|
26
|
+
policy: AssurancePolicy;
|
|
27
|
+
headSha: string | null;
|
|
28
|
+
/** Change reviews whose head commit is HEAD or one of its ancestors (their findings apply to this commit). */
|
|
29
|
+
relevantReviewIds: string[];
|
|
30
|
+
now: Date;
|
|
31
|
+
}
|
|
32
|
+
/** Accepted-risk status at a date. */
|
|
33
|
+
export declare function acceptanceStatus(expires: string, reviewDate: string, now: Date): "ACTIVE" | "REVIEW_DUE" | "EXPIRED";
|
|
34
|
+
/** Phase progress (VC-P0…P7), from what was actually produced. Unexecuted phases are never shown as complete. */
|
|
35
|
+
export declare function phaseProgress(i: GateInput): PhaseProgress[];
|
|
36
|
+
/** Evaluates the release gate. */
|
|
37
|
+
export declare function evaluateGate(i: GateInput): ReleaseGate;
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { isOpen } from "../scoring/score.js";
|
|
2
|
+
import { stableId } from "../util/hash.js";
|
|
3
|
+
import { tallyApprovals } from "./approvals.js";
|
|
4
|
+
import { ASSURANCE_SCHEMA_VERSION, METHODOLOGY_VERSION, } from "./types.js";
|
|
5
|
+
import { daysUntil, digestOf } from "./util.js";
|
|
6
|
+
const RESOLVED = new Set(["CLOSED", "VERIFIED_FIXED", "REJECTED_FALSE_POSITIVE"]);
|
|
7
|
+
/** Accepted-risk status at a date. */
|
|
8
|
+
export function acceptanceStatus(expires, reviewDate, now) {
|
|
9
|
+
if (daysUntil(expires, now) < 0)
|
|
10
|
+
return "EXPIRED";
|
|
11
|
+
if (daysUntil(reviewDate, now) < 0)
|
|
12
|
+
return "REVIEW_DUE";
|
|
13
|
+
return "ACTIVE";
|
|
14
|
+
}
|
|
15
|
+
/** Phase progress (VC-P0…P7), from what was actually produced. Unexecuted phases are never shown as complete. */
|
|
16
|
+
export function phaseProgress(i) {
|
|
17
|
+
const r = i.results;
|
|
18
|
+
const reg = i.register;
|
|
19
|
+
const runs = reg?.verification_runs ?? [];
|
|
20
|
+
const p = (phase, name, status, evidence) => ({ phase, name, status, evidence });
|
|
21
|
+
return [
|
|
22
|
+
p("VC-P0", "Intake and baseline", r.run.status === "COMPLETED" ? "COMPLETED" : "PARTIAL", [
|
|
23
|
+
`Scan ${r.run.run_id} (${r.run.status}, ${r.run.mode_effective ?? "no mode"}, ${r.run.tier}) at ${r.run.target_revision.repository?.commit?.slice(0, 12) ?? "no commit"}; PoP ${r.pop_coverage.summary.available} available / ${r.pop_coverage.summary.blocked} blocked.`,
|
|
24
|
+
]),
|
|
25
|
+
i.proof
|
|
26
|
+
? p("VC-P1", "Author-side proof", i.proof.result === "INCONCLUSIVE" ? "PARTIAL" : "COMPLETED", [
|
|
27
|
+
`Proof ${i.proof.verification_id}: ${i.proof.result}, ${i.proof.executions.filter((e) => e.status === "PASS").length}/${i.proof.executions.length} command(s) passed at ${i.proof.exact_sha?.slice(0, 12) ?? "?"}.`,
|
|
28
|
+
])
|
|
29
|
+
: p("VC-P1", "Author-side proof", "NOT_STARTED", ["No proof run (product-check proof --allow-exec)."]),
|
|
30
|
+
i.review
|
|
31
|
+
? p("VC-P2", "Layered independent review", i.review.lanes.some((l) => l.status === "NOT_TESTED") ? "PARTIAL" : "COMPLETED", [
|
|
32
|
+
`Review ${i.review.review_id}: ${i.review.findings.length} finding(s), ${i.review.disproven.length} disproven; lanes not run: ${i.review.lanes
|
|
33
|
+
.filter((l) => l.status === "NOT_TESTED")
|
|
34
|
+
.map((l) => l.lane)
|
|
35
|
+
.join(", ") || "none"}.`,
|
|
36
|
+
])
|
|
37
|
+
: reg
|
|
38
|
+
? p("VC-P2", "Layered independent review", "PARTIAL", [
|
|
39
|
+
`Baseline findings independently verified in the register (${reg.entries.length}); no change review.`,
|
|
40
|
+
])
|
|
41
|
+
: p("VC-P2", "Layered independent review", "NOT_STARTED", []),
|
|
42
|
+
i.review
|
|
43
|
+
? p("VC-P3", "Risk-routed gate", i.review.gate.status === "NOT_EVALUATED" ? "PARTIAL" : "COMPLETED", [
|
|
44
|
+
`Change risk ${i.review.risk.level}, gate ${i.review.gate.status}, ${i.review.approvals.filter((a) => a.status === "VALID").length}/${i.review.approvals_required.humans} valid approval(s).`,
|
|
45
|
+
])
|
|
46
|
+
: p("VC-P3", "Risk-routed gate", "NOT_STARTED", ["No change review to route."]),
|
|
47
|
+
i.packets.length > 0
|
|
48
|
+
? p("VC-P4", "Blueprint and repair", "COMPLETED", [
|
|
49
|
+
`${i.packets.length} fix packet(s); blueprint from scan ${r.run.run_id}.`,
|
|
50
|
+
])
|
|
51
|
+
: p("VC-P4", "Blueprint and repair", r.blueprint.remediations.length > 0 ? "PARTIAL" : "NOT_STARTED", [
|
|
52
|
+
"No fix packets generated.",
|
|
53
|
+
]),
|
|
54
|
+
runs.length > 0
|
|
55
|
+
? p("VC-P5", "Closure verification", runs.some((x) => x.result === "PASS") ? "COMPLETED" : "PARTIAL", [
|
|
56
|
+
`${runs.length} verification run(s): ${runs.filter((x) => x.result === "PASS").length} PASS, ${runs.filter((x) => x.result === "FAIL").length} FAIL, ${runs.filter((x) => x.result === "INCONCLUSIVE").length} INCONCLUSIVE.`,
|
|
57
|
+
])
|
|
58
|
+
: p("VC-P5", "Closure verification", "NOT_STARTED", []),
|
|
59
|
+
p("VC-P6", "Merge and release", "PARTIAL", [
|
|
60
|
+
"Release gate evaluated against the current commit with approval staleness. Merge-queue simulation, staged rollout and live canary watch are not executed by Product Check (declare a rollout plan; watch is NOT_SUPPORTED).",
|
|
61
|
+
]),
|
|
62
|
+
(reg?.regression_corpus.length ?? 0) > 0 ||
|
|
63
|
+
(reg?.entries.some((e) => e.state === "REJECTED_FALSE_POSITIVE" || e.regressions.length > 0) ?? false)
|
|
64
|
+
? p("VC-P7", "Learn and assure", "PARTIAL", [
|
|
65
|
+
`${reg?.regression_corpus.length ?? 0} regression corpus case(s); ${reg?.entries.filter((e) => e.state === "REJECTED_FALSE_POSITIVE").length ?? 0} false positive(s); ${reg?.entries.filter((e) => e.regressions.length > 0).length ?? 0} regressed finding(s). Telemetry-driven incident import is not supported.`,
|
|
66
|
+
])
|
|
67
|
+
: p("VC-P7", "Learn and assure", "NOT_STARTED", []),
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
/** Evaluates the release gate. */
|
|
71
|
+
export function evaluateGate(i) {
|
|
72
|
+
const { results: r, register: reg, review, proof, policy, now } = i;
|
|
73
|
+
const conditions = [];
|
|
74
|
+
const c = (id, title, status, detail, refs = []) => {
|
|
75
|
+
conditions.push({ id, title, status, detail, refs });
|
|
76
|
+
};
|
|
77
|
+
// Open findings: the register (with closure states) supersedes the raw results when present.
|
|
78
|
+
const open = { P0: 0, P1: 0, P2: 0, P3: 0 };
|
|
79
|
+
const accepted = (reg?.risk_acceptances ?? []).map((a) => ({
|
|
80
|
+
acceptance_id: a.acceptance_id,
|
|
81
|
+
finding_id: a.finding_id,
|
|
82
|
+
expires: a.expires,
|
|
83
|
+
status: acceptanceStatus(a.expires, a.review_date, now),
|
|
84
|
+
}));
|
|
85
|
+
const blockingIds = [];
|
|
86
|
+
const p1Unaccepted = [];
|
|
87
|
+
const triageIds = [];
|
|
88
|
+
if (reg) {
|
|
89
|
+
for (const e of reg.entries) {
|
|
90
|
+
if (RESOLVED.has(e.state))
|
|
91
|
+
continue;
|
|
92
|
+
if (e.source === "change-review" && !i.relevantReviewIds.includes(e.opened_in))
|
|
93
|
+
continue;
|
|
94
|
+
const acc = accepted.find((a) => a.finding_id === e.finding_id && e.state === "ACCEPTED_RISK");
|
|
95
|
+
if (acc && acc.status !== "EXPIRED")
|
|
96
|
+
continue;
|
|
97
|
+
open[e.severity]++;
|
|
98
|
+
// Unverified P1 claims never drive a block (they need triage); P0 fails closed per policy.
|
|
99
|
+
const verified = ["CONFIRMED", "CORROBORATED", "REPRODUCED"].includes(e.verification.state);
|
|
100
|
+
if (e.severity === "P0" &&
|
|
101
|
+
(verified || (policy.review.fail_closed_unverified_p0 && e.verification.state !== "DISPROVEN")))
|
|
102
|
+
blockingIds.push(e.finding_id);
|
|
103
|
+
else if (e.severity === "P1" && verified) {
|
|
104
|
+
blockingIds.push(e.finding_id);
|
|
105
|
+
p1Unaccepted.push(e.finding_id);
|
|
106
|
+
}
|
|
107
|
+
else if (e.severity === "P0" || e.severity === "P1")
|
|
108
|
+
triageIds.push(e.finding_id);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
for (const f of r.findings.filter(isOpen)) {
|
|
113
|
+
open[f.severity]++;
|
|
114
|
+
if (f.severity === "P0" || f.severity === "P1")
|
|
115
|
+
blockingIds.push(f.id);
|
|
116
|
+
if (f.severity === "P1")
|
|
117
|
+
p1Unaccepted.push(f.id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
const reviewBlock = review?.findings.filter((f) => f.blocking) ?? [];
|
|
121
|
+
const scanSha = r.run.target_revision.repository?.commit ?? null;
|
|
122
|
+
const staleScan = scanSha !== null && i.headSha !== null && scanSha !== i.headSha;
|
|
123
|
+
c("RC-0", "Product scan describes the head commit", i.headSha === null || scanSha === null ? "NOT_EVALUATED" : staleScan ? "NOT_EVALUATED" : "PASS", staleScan
|
|
124
|
+
? `The product scan is for ${scanSha?.slice(0, 12)}, HEAD is ${i.headSha?.slice(0, 12)}: its verdict and gates describe an older commit. Re-run: product-check retest --baseline <results> --repo . --out-dir <workspace>, then product-check register.`
|
|
125
|
+
: i.headSha === null || scanSha === null
|
|
126
|
+
? "Scan commit or HEAD unknown."
|
|
127
|
+
: `Scan ${r.run.run_id} is at HEAD.`);
|
|
128
|
+
const failedGates = staleScan
|
|
129
|
+
? []
|
|
130
|
+
: r.scorecard.gates.filter((g) => g.status === "FAIL").map((g) => g.gate_id);
|
|
131
|
+
c("RC-1", "No open P0 or failed hard gate", !blockingIds.some((id) => reg?.entries.find((e) => e.finding_id === id)?.severity === "P0" ||
|
|
132
|
+
r.findings.find((f) => f.id === id)?.severity === "P0") &&
|
|
133
|
+
failedGates.length === 0 &&
|
|
134
|
+
!reviewBlock.some((f) => f.severity === "P0")
|
|
135
|
+
? "PASS"
|
|
136
|
+
: "FAIL", `${open.P0} open P0; failed gates: ${failedGates.join(", ") || "none"}${staleScan ? " (scan is stale; gates not re-checked at HEAD)" : ""}; change-review P0 blockers: ${reviewBlock.filter((f) => f.severity === "P0").length}.`, blockingIds.filter((x) => x));
|
|
137
|
+
const expired = accepted.filter((a) => a.status === "EXPIRED");
|
|
138
|
+
c("RC-2", "P1 handled under the CT3 policy (fixed, or a valid unexpired acceptance)", p1Unaccepted.length === 0 && expired.length === 0 && !reviewBlock.some((f) => f.severity === "P1")
|
|
139
|
+
? "PASS"
|
|
140
|
+
: "FAIL", `${p1Unaccepted.length} P1 without a valid acceptance; ${expired.length} expired acceptance(s); change-review P1 blockers: ${reviewBlock.filter((f) => f.severity === "P1").length}.`, p1Unaccepted);
|
|
141
|
+
const proofAtHead = proof && i.headSha !== null && proof.exact_sha === i.headSha ? proof : null;
|
|
142
|
+
c("RC-3", "Required checks executed and green at the head commit", policy.required_checks.length === 0
|
|
143
|
+
? "NOT_EVALUATED"
|
|
144
|
+
: !proofAtHead
|
|
145
|
+
? "NOT_EVALUATED"
|
|
146
|
+
: proofAtHead.result === "PASS"
|
|
147
|
+
? "PASS"
|
|
148
|
+
: proofAtHead.result === "FAIL"
|
|
149
|
+
? "FAIL"
|
|
150
|
+
: "NOT_EVALUATED", policy.required_checks.length === 0
|
|
151
|
+
? "The policy declares no required checks."
|
|
152
|
+
: proofAtHead
|
|
153
|
+
? `Proof ${proofAtHead.verification_id}: ${proofAtHead.result}.`
|
|
154
|
+
: `No proof run at ${i.headSha?.slice(0, 12) ?? "an unknown head"}.`, proofAtHead ? [proofAtHead.verification_id] : []);
|
|
155
|
+
const allApprovals = [
|
|
156
|
+
...new Map([...(reg?.approvals ?? []), ...i.approvals].map((a) => [a.approval_id, a])).values(),
|
|
157
|
+
];
|
|
158
|
+
const required = review ? review.approvals_required.humans : policy.risk.approvals.MEDIUM;
|
|
159
|
+
const tally = i.headSha === null
|
|
160
|
+
? null
|
|
161
|
+
: review
|
|
162
|
+
? tallyApprovals(allApprovals, policy, {
|
|
163
|
+
scopeType: "change",
|
|
164
|
+
ref: review.change_ref,
|
|
165
|
+
sha: i.headSha,
|
|
166
|
+
excluded: review.author.id ? [review.author.id] : [],
|
|
167
|
+
}, required, review.approvals_required.domain_owners)
|
|
168
|
+
: tallyApprovals(allApprovals, policy, { scopeType: "release", ref: r.contract.target.name, sha: i.headSha, excluded: [] }, required);
|
|
169
|
+
const reviewStale = review !== null && i.headSha !== null && review.head_sha !== i.headSha;
|
|
170
|
+
c("RC-4", "Human approvals valid for the head commit", tally === null ? "NOT_EVALUATED" : tally.satisfied && !reviewStale ? "PASS" : "FAIL", tally === null
|
|
171
|
+
? "Head commit unknown."
|
|
172
|
+
: reviewStale
|
|
173
|
+
? `The change review covers ${review?.head_sha?.slice(0, 12) ?? "?"}, but HEAD is ${i.headSha?.slice(0, 12) ?? "?"}; re-review and re-approve.`
|
|
174
|
+
: `${tally.validApprovers.length}/${required} valid signed approval(s) (scope ${review ? `change:${review.change_ref}` : `release:${r.contract.target.name}`})${tally.stale ? `, ${tally.stale} stale` : ""}${tally.missingOwners.length ? `; domain owner(s) missing: ${tally.missingOwners.join(", ")}` : ""}.`);
|
|
175
|
+
const gaps = [
|
|
176
|
+
...r.pop_coverage.pops
|
|
177
|
+
.filter((p) => p.relevant && (p.status === "BLOCKED" || p.status === "PARTIAL"))
|
|
178
|
+
.map((p) => `${p.name}: ${p.status}${p.limitations[0] ? ` (${p.limitations[0]})` : ""}`),
|
|
179
|
+
...r.scorecard.not_tested_controls.map((x) => `${x} not tested`),
|
|
180
|
+
...r.scorecard.blocked_controls.map((x) => `${x} blocked`),
|
|
181
|
+
];
|
|
182
|
+
c("RC-5", "Coverage gaps disclosed", "PASS", `${gaps.length} gap(s) disclosed below; overall coverage ${Math.round(r.scorecard.overall_coverage * 100)}%.`);
|
|
183
|
+
const pending = reg?.entries.filter((e) => e.state === "RETEST_REQUIRED" || e.state === "FIX_PROPOSED") ?? [];
|
|
184
|
+
c("RC-6", "Targeted fixes retested", !reg ? "NOT_EVALUATED" : pending.length === 0 ? "PASS" : "FAIL", !reg
|
|
185
|
+
? "No closure register."
|
|
186
|
+
: pending.length === 0
|
|
187
|
+
? "No fix is waiting for verification."
|
|
188
|
+
: `Awaiting verification: ${pending.map((e) => e.finding_id).join(", ")}.`, pending.map((e) => e.finding_id));
|
|
189
|
+
const material = review?.risk.effective_level === "HIGH" ||
|
|
190
|
+
accepted.length > 0 ||
|
|
191
|
+
["CT-T3", "CT-T4", "CT-T5", "CT-T6"].includes(r.run.tier);
|
|
192
|
+
c("RC-7", "Rollback or containment available for material risk", policy.release.rollback_plan ? "PASS" : material ? "FAIL" : "NOT_EVALUATED", policy.release.rollback_plan
|
|
193
|
+
? `Rollback plan: ${policy.release.rollback_plan}${policy.release.staged_rollout ? `; staged rollout: ${policy.release.staged_rollout}` : ""}.`
|
|
194
|
+
: material
|
|
195
|
+
? "Material risk (HIGH change, accepted risk or CT-T3+) and no rollback plan declared in the policy."
|
|
196
|
+
: "No rollback plan declared; no material risk identified.");
|
|
197
|
+
const regressedEntries = reg?.entries.filter((e) => e.state === "REGRESSED") ?? [];
|
|
198
|
+
const regressedControls = r.comparison?.regressed_controls ?? [];
|
|
199
|
+
c("RC-8", "No material regression", regressedEntries.length > 0 || regressedControls.length > 0
|
|
200
|
+
? "FAIL"
|
|
201
|
+
: reg || r.comparison
|
|
202
|
+
? "PASS"
|
|
203
|
+
: "NOT_EVALUATED", regressedEntries.length > 0 || regressedControls.length > 0
|
|
204
|
+
? `Regressed: ${[...regressedEntries.map((e) => e.finding_id), ...regressedControls].join(", ")}.`
|
|
205
|
+
: reg || r.comparison
|
|
206
|
+
? "No regressed finding or control."
|
|
207
|
+
: "No history to compare against.");
|
|
208
|
+
// Executed evidence -------------------------------------------------------------------------------------------
|
|
209
|
+
const executions = [
|
|
210
|
+
...(proof?.executions ?? []),
|
|
211
|
+
...(reg?.verification_runs.flatMap((v) => v.executions) ?? []),
|
|
212
|
+
];
|
|
213
|
+
const executedTests = r.evidence.filter((e) => e.strength === "executed-test" || e.strength === "runtime").length;
|
|
214
|
+
const executed = {
|
|
215
|
+
count: executions.length,
|
|
216
|
+
passed: executions.filter((e) => e.status === "PASS").length,
|
|
217
|
+
failed: executions.filter((e) => e.status === "FAIL").length,
|
|
218
|
+
not_run: executions.filter((e) => e.status === "NOT_TESTED" || e.status === "BLOCKED").length,
|
|
219
|
+
};
|
|
220
|
+
const anythingExecuted = executed.passed + executed.failed > 0 ||
|
|
221
|
+
executedTests > 0 ||
|
|
222
|
+
(reg?.verification_runs.some((v) => v.after_results !== null) ?? false);
|
|
223
|
+
const reasons = [];
|
|
224
|
+
// Substantiated blockers decide first; then missing evidence; then unmet release preconditions (approvals, rollback).
|
|
225
|
+
const PRECONDITIONS = new Set(["RC-4", "RC-7"]);
|
|
226
|
+
const hardFails = conditions.filter((x) => x.status === "FAIL" && !PRECONDITIONS.has(x.id));
|
|
227
|
+
const preFails = conditions.filter((x) => x.status === "FAIL" && PRECONDITIONS.has(x.id));
|
|
228
|
+
const productNotReady = !staleScan && r.verdict.verdict === "NOT_READY";
|
|
229
|
+
const insufficient = staleScan ||
|
|
230
|
+
!anythingExecuted ||
|
|
231
|
+
r.verdict.verdict === "INCONCLUSIVE" ||
|
|
232
|
+
r.scorecard.overall_coverage < r.contract.scoring.min_coverage ||
|
|
233
|
+
conditions.some((x) => x.id === "RC-3" && x.status === "NOT_EVALUATED") ||
|
|
234
|
+
conditions.some((x) => x.id === "RC-4" && x.status === "NOT_EVALUATED");
|
|
235
|
+
let decision;
|
|
236
|
+
if (hardFails.length > 0 || productNotReady || review?.gate.status === "BLOCK") {
|
|
237
|
+
decision = "NO_GO";
|
|
238
|
+
reasons.push(...hardFails.map((x) => `${x.id} ${x.title}: ${x.detail}`));
|
|
239
|
+
if (productNotReady)
|
|
240
|
+
reasons.push(`Product verdict NOT_READY: ${r.verdict.reasons.join(" ")}`);
|
|
241
|
+
if (review?.gate.status === "BLOCK")
|
|
242
|
+
reasons.push(`Change gate BLOCK: ${review.gate.reasons.join(" ")}`);
|
|
243
|
+
reasons.push(...preFails.map((x) => `${x.id} ${x.title}: ${x.detail}`));
|
|
244
|
+
}
|
|
245
|
+
else if (insufficient) {
|
|
246
|
+
decision = "UNKNOWN";
|
|
247
|
+
if (!anythingExecuted)
|
|
248
|
+
reasons.push("INSUFFICIENT EVIDENCE: no check was executed (no proof run, no executed tests, no verification). A clean static scan is not a pass.");
|
|
249
|
+
if (staleScan)
|
|
250
|
+
reasons.push(`Product scan is stale (scan ${scanSha?.slice(0, 12)}, HEAD ${i.headSha?.slice(0, 12)}); re-scan before a release decision.`);
|
|
251
|
+
if (r.verdict.verdict === "INCONCLUSIVE")
|
|
252
|
+
reasons.push(`Product verdict INCONCLUSIVE: ${r.verdict.reasons.join(" ")}`);
|
|
253
|
+
if (r.scorecard.overall_coverage < r.contract.scoring.min_coverage)
|
|
254
|
+
reasons.push(`Coverage ${Math.round(r.scorecard.overall_coverage * 100)}% is below the minimum ${Math.round(r.contract.scoring.min_coverage * 100)}%.`);
|
|
255
|
+
for (const x of conditions.filter((y) => (y.id === "RC-3" || y.id === "RC-4") && y.status === "NOT_EVALUATED"))
|
|
256
|
+
reasons.push(`${x.id} not evaluated: ${x.detail}`);
|
|
257
|
+
reasons.push(...preFails.map((x) => `${x.id} ${x.title}: ${x.detail}`));
|
|
258
|
+
}
|
|
259
|
+
else if (preFails.length > 0) {
|
|
260
|
+
decision = "NO_GO";
|
|
261
|
+
reasons.push(...preFails.map((x) => `${x.id} ${x.title}: ${x.detail}`));
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const conds = [];
|
|
265
|
+
if (r.verdict.verdict === "READY_WITH_CONDITIONS")
|
|
266
|
+
conds.push(`Product verdict READY_WITH_CONDITIONS: ${r.verdict.reasons.join(" ")}`);
|
|
267
|
+
if (accepted.some((a) => a.status !== "EXPIRED"))
|
|
268
|
+
conds.push(`${accepted.filter((a) => a.status !== "EXPIRED").length} accepted risk(s)${accepted.some((a) => a.status === "REVIEW_DUE") ? ", review due" : ""}.`);
|
|
269
|
+
if (open.P2 > 0)
|
|
270
|
+
conds.push(`${open.P2} open P2 finding(s).`);
|
|
271
|
+
if (triageIds.length > 0)
|
|
272
|
+
conds.push(`${triageIds.length} unverified P0/P1 claim(s) need human triage (not proven defects): ${triageIds.join(", ")}.`);
|
|
273
|
+
if (review?.gate.status === "CONDITIONAL" || review?.gate.status === "NOT_EVALUATED")
|
|
274
|
+
conds.push(`Change gate ${review.gate.status}: ${review.gate.reasons.join(" ")}`);
|
|
275
|
+
for (const x of conditions.filter((y) => y.status === "NOT_EVALUATED"))
|
|
276
|
+
conds.push(`${x.id} not evaluated: ${x.detail}`);
|
|
277
|
+
decision = conds.length > 0 ? "CONDITIONAL_GO" : "GO";
|
|
278
|
+
reasons.push(...(conds.length > 0
|
|
279
|
+
? conds
|
|
280
|
+
: ["Every release condition passed with executed evidence at the head commit."]));
|
|
281
|
+
}
|
|
282
|
+
const registerDigest = reg ? digestOf(reg) : null;
|
|
283
|
+
return {
|
|
284
|
+
schema_version: ASSURANCE_SCHEMA_VERSION,
|
|
285
|
+
kind: "release-gate",
|
|
286
|
+
methodology_version: METHODOLOGY_VERSION,
|
|
287
|
+
gate_id: stableId("RG", [
|
|
288
|
+
i.resultsSha,
|
|
289
|
+
registerDigest,
|
|
290
|
+
review?.review_id ?? null,
|
|
291
|
+
proof?.verification_id ?? null,
|
|
292
|
+
i.headSha,
|
|
293
|
+
now.toISOString(),
|
|
294
|
+
]),
|
|
295
|
+
evaluated_at: now.toISOString(),
|
|
296
|
+
target: { name: r.contract.target.name, repository: r.run.target_revision.repository?.label ?? null },
|
|
297
|
+
head_sha: i.headSha,
|
|
298
|
+
decision,
|
|
299
|
+
product_verdict: r.verdict.verdict,
|
|
300
|
+
change_gate: review?.gate.status ?? null,
|
|
301
|
+
conditions,
|
|
302
|
+
blocking_reasons: reasons,
|
|
303
|
+
open,
|
|
304
|
+
open_blocking_ids: blockingIds,
|
|
305
|
+
accepted_risks: accepted,
|
|
306
|
+
approvals: { required, valid: tally?.validApprovers.length ?? 0, stale: tally?.stale ?? 0 },
|
|
307
|
+
executed_checks: executed,
|
|
308
|
+
coverage: {
|
|
309
|
+
customer: r.scorecard.pillars.customer.coverage,
|
|
310
|
+
technology: r.scorecard.pillars.technology.coverage,
|
|
311
|
+
trust: r.scorecard.pillars.trust.coverage,
|
|
312
|
+
overall: r.scorecard.overall_coverage,
|
|
313
|
+
gaps,
|
|
314
|
+
},
|
|
315
|
+
rollback: { declared: policy.release.rollback_plan !== null, ref: policy.release.rollback_plan },
|
|
316
|
+
phases: phaseProgress(i),
|
|
317
|
+
inputs: {
|
|
318
|
+
results_sha256: i.resultsSha,
|
|
319
|
+
register_digest: registerDigest,
|
|
320
|
+
review_id: review?.review_id ?? null,
|
|
321
|
+
proof_run_id: proof?.verification_id ?? null,
|
|
322
|
+
},
|
|
323
|
+
};
|
|
324
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic review lanes over a change. Each lane is an independent reviewer with its own rules; none needs
|
|
3
|
+
* credentials or network access. Rules look at what the change adds or removes, so a review of a diff does not
|
|
4
|
+
* re-report the whole product (the baseline scan does that). Rules that only match a pattern are hypotheses and
|
|
5
|
+
* go to human triage unless a second, independent signal corroborates them.
|
|
6
|
+
*
|
|
7
|
+
* Covered vibe-coding failure modes: client-side secrets, removed authorization, object access without an
|
|
8
|
+
* ownership filter, stubbed APIs and silent fallbacks, mock data in production code, tests that only assert mocks,
|
|
9
|
+
* focused/skipped tests, TLS verification disabled, wildcard CORS, string-built SQL, eval of dynamic input, model
|
|
10
|
+
* output reaching a shell or eval, non-idempotent payments, irreversible migrations, images without alt text.
|
|
11
|
+
*/
|
|
12
|
+
import type { Confidence, Epistemic, Pillar, Severity } from "../types.js";
|
|
13
|
+
import type { DiffFile } from "./diff.js";
|
|
14
|
+
import type { Lane } from "./types.js";
|
|
15
|
+
/** One rule hit before verification. */
|
|
16
|
+
export interface RuleHit {
|
|
17
|
+
rule_id: string;
|
|
18
|
+
lane: Lane;
|
|
19
|
+
title: string;
|
|
20
|
+
severity: Severity;
|
|
21
|
+
pillars: Pillar[];
|
|
22
|
+
epistemic: Epistemic;
|
|
23
|
+
confidence: Confidence;
|
|
24
|
+
path: string;
|
|
25
|
+
line: number | null;
|
|
26
|
+
/** Normalized text of what matched (redacted later), used for the root-cause signature. */
|
|
27
|
+
signature: string;
|
|
28
|
+
expected: string;
|
|
29
|
+
actual: string;
|
|
30
|
+
impact: string;
|
|
31
|
+
excerpt: string;
|
|
32
|
+
}
|
|
33
|
+
interface LineRule {
|
|
34
|
+
id: string;
|
|
35
|
+
lane: Lane;
|
|
36
|
+
title: string;
|
|
37
|
+
severity: Severity;
|
|
38
|
+
pillars: Pillar[];
|
|
39
|
+
epistemic: Epistemic;
|
|
40
|
+
confidence: Confidence;
|
|
41
|
+
expected: string;
|
|
42
|
+
impact: string;
|
|
43
|
+
/** Which files the rule applies to. */
|
|
44
|
+
files: (path: string) => boolean;
|
|
45
|
+
/** Returns the matched text, or null. */
|
|
46
|
+
match: (text: string, path: string) => string | null;
|
|
47
|
+
}
|
|
48
|
+
/** Line rules: evaluated on each added line (and on whole files when re-checking a fix). */
|
|
49
|
+
export declare const LINE_RULES: LineRule[];
|
|
50
|
+
/** Runs the line rules over a set of lines of one file. */
|
|
51
|
+
export declare function lineHits(path: string, lines: {
|
|
52
|
+
line: number | null;
|
|
53
|
+
text: string;
|
|
54
|
+
}[]): RuleHit[];
|
|
55
|
+
/** File-level rules that need the whole change of a file (removed auth, ownership filters, migrations, tests). */
|
|
56
|
+
export declare function fileHits(file: DiffFile, headText: string | null): RuleHit[];
|
|
57
|
+
/** Change-level rule: production code changed but no test changed. */
|
|
58
|
+
export declare function changeHits(files: DiffFile[]): RuleHit[];
|
|
59
|
+
/** True when a path is client-reachable code (used to raise secret findings' certainty). */
|
|
60
|
+
export declare const isClientPath: (p: string) => boolean;
|
|
61
|
+
export {};
|