@vantageos/backend-doctor 0.2.0 → 0.4.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 +57 -4
- package/dist/args.d.ts +1 -1
- package/dist/args.js +19 -1
- package/dist/build-stamp.json +2 -2
- package/dist/caller-resolution.js +11 -8
- package/dist/cli.js +164 -15
- package/dist/contract.d.ts +25 -7
- package/dist/contract.js +52 -7
- package/dist/coverage.d.ts +1 -1
- package/dist/coverage.js +2 -2
- package/dist/detectors/oracle-axis1.js +5 -4
- package/dist/detectors/oracle-axis2.d.ts +6 -0
- package/dist/detectors/oracle-axis2.js +32 -16
- package/dist/detectors/predicates.js +12 -3
- package/dist/detectors/shared.js +3 -2
- package/dist/detectors/source/_ast.d.ts +92 -0
- package/dist/detectors/source/_ast.js +353 -0
- package/dist/detectors/source/_scan.js +4 -3
- package/dist/detectors/source/backfill.js +62 -17
- package/dist/detectors/source/contract.js +21 -13
- package/dist/detectors/source/credential-hygiene.d.ts +11 -0
- package/dist/detectors/source/credential-hygiene.js +619 -0
- package/dist/detectors/source/cross-tenant-write.d.ts +46 -0
- package/dist/detectors/source/cross-tenant-write.js +1408 -0
- package/dist/detectors/source/guard-helper-flow.d.ts +22 -0
- package/dist/detectors/source/guard-helper-flow.js +49 -6
- package/dist/detectors/source/identity-by-id.d.ts +80 -0
- package/dist/detectors/source/identity-by-id.js +1783 -0
- package/dist/detectors/source/identity.d.ts +8 -1
- package/dist/detectors/source/identity.js +364 -35
- package/dist/detectors/source/jobs.js +13 -2
- package/dist/detectors/source/lifecycle.js +14 -2
- package/dist/detectors/source/local-identity.d.ts +26 -0
- package/dist/detectors/source/local-identity.js +666 -0
- package/dist/detectors/source/narrow-then-bound.d.ts +11 -0
- package/dist/detectors/source/narrow-then-bound.js +571 -0
- package/dist/detectors/source/package-principal.d.ts +9 -0
- package/dist/detectors/source/package-principal.js +177 -0
- package/dist/detectors/source/r6-shapes.d.ts +20 -0
- package/dist/detectors/source/r6-shapes.js +285 -0
- package/dist/detectors/source/readbudget.js +291 -0
- package/dist/detectors/source/recipient-by-id.d.ts +40 -0
- package/dist/detectors/source/recipient-by-id.js +1108 -0
- package/dist/detectors/source/refusal.d.ts +14 -0
- package/dist/detectors/source/refusal.js +75 -2
- package/dist/detectors/source/security.js +455 -26
- package/dist/detectors/source/service-account-claim.d.ts +15 -0
- package/dist/detectors/source/service-account-claim.js +491 -0
- package/dist/detectors/source/writesafety.js +27 -14
- package/dist/detectors/standard/_doc.d.ts +1 -1
- package/dist/detectors/standard/_doc.js +5 -4
- package/dist/detectors/standard/r41-frontier.js +3 -2
- package/dist/detectors/standard/r46-returns-validator.js +3 -2
- package/dist/detectors/structural.d.ts +6 -7
- package/dist/detectors/structural.js +75 -43
- package/dist/not-checkable.d.ts +1 -1
- package/dist/not-checkable.js +12 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +55 -3
- package/dist/rule-class.d.ts +85 -0
- package/dist/rule-class.js +218 -0
- package/dist/tally.d.ts +13 -5
- package/dist/tally.js +14 -6
- package/dist/violation-count.d.ts +48 -0
- package/dist/violation-count.js +94 -0
- package/fixtures/backend-standard-2825122.md +722 -0
- package/fixtures/backend-standard-4dcd3e0.md +722 -0
- package/fixtures/backend-standard-bacc54a.md +785 -0
- package/fixtures/rule-classes-bacc54a.json +244 -0
- package/package.json +1 -1
package/dist/tally.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ export interface Verdict {
|
|
|
23
23
|
exitCode: number;
|
|
24
24
|
/** non-conforming rules bucketed by severity. */
|
|
25
25
|
violations: Record<Severity, string[]>;
|
|
26
|
-
/** could-not-judge rules bucketed by severity — abstentions,
|
|
26
|
+
/** could-not-judge rules bucketed by severity — abstentions: the instrument did not decide, and each names why. A disposition-only abstention is retired (k179jnhx); see the ABSTENTIONS note below for what remains. */
|
|
27
27
|
abstentions: Record<Severity, string[]>;
|
|
28
28
|
/** derived counts the wording turns on. */
|
|
29
29
|
criticalViolations: number;
|
|
@@ -40,14 +40,22 @@ export interface Verdict {
|
|
|
40
40
|
* 0 critical + 1..5 major violations -> revise recommended
|
|
41
41
|
* 0 critical + >5 major violations -> revise mandatory
|
|
42
42
|
* >=1 critical violation -> revise mandatory (NO bypass)
|
|
43
|
-
* ABSTENTIONS: a could-not-judge
|
|
44
|
-
*
|
|
45
|
-
*
|
|
43
|
+
* ABSTENTIONS: a could-not-judge means the instrument did NOT decide, and says
|
|
44
|
+
* why. Task k179jnhx retired judgement as an acceptance kind: a rule whose
|
|
45
|
+
* DISPOSITION used to wait on a person now decides on a written justification
|
|
46
|
+
* token's PRESENCE. What still abstains is named per rule, never relabelled:
|
|
47
|
+
* an unreadable input (unparseable file, unreachable tree, absent oracle), a
|
|
48
|
+
* rule with no detector yet (pinned NOT_MECHANICALLY_CHECKABLE), a population
|
|
49
|
+
* the source cannot decide statically, or a recorded marker claim the standard
|
|
50
|
+
* itself sends to a reviewer (R-51's write-contract marker).
|
|
51
|
+
* Any abstention still blocks "approve" (the outcome becomes at least
|
|
52
|
+
* revise-recommended); an abstention on a CRITICAL rule can never be silently
|
|
53
|
+
* approved and is reported first.
|
|
46
54
|
*
|
|
47
55
|
* EXIT CODE preserves the engine's honesty:
|
|
48
56
|
* >=1 critical OR revise-mandatory -> 1 (a violation IS wrong)
|
|
49
57
|
* 1..5 major violations (revise-recommended) -> 1 (hard violations exist)
|
|
50
|
-
* no hard violation,
|
|
58
|
+
* no hard violation, input unreadable somewhere -> 2 (not read = never a clean 0)
|
|
51
59
|
* approve -> 0
|
|
52
60
|
* We cross-check against the engine's computeExitCode and never print "approve"
|
|
53
61
|
* on an exit that means abstention.
|
package/dist/tally.js
CHANGED
|
@@ -71,14 +71,22 @@ function emptyBySeverity() {
|
|
|
71
71
|
* 0 critical + 1..5 major violations -> revise recommended
|
|
72
72
|
* 0 critical + >5 major violations -> revise mandatory
|
|
73
73
|
* >=1 critical violation -> revise mandatory (NO bypass)
|
|
74
|
-
* ABSTENTIONS: a could-not-judge
|
|
75
|
-
*
|
|
76
|
-
*
|
|
74
|
+
* ABSTENTIONS: a could-not-judge means the instrument did NOT decide, and says
|
|
75
|
+
* why. Task k179jnhx retired judgement as an acceptance kind: a rule whose
|
|
76
|
+
* DISPOSITION used to wait on a person now decides on a written justification
|
|
77
|
+
* token's PRESENCE. What still abstains is named per rule, never relabelled:
|
|
78
|
+
* an unreadable input (unparseable file, unreachable tree, absent oracle), a
|
|
79
|
+
* rule with no detector yet (pinned NOT_MECHANICALLY_CHECKABLE), a population
|
|
80
|
+
* the source cannot decide statically, or a recorded marker claim the standard
|
|
81
|
+
* itself sends to a reviewer (R-51's write-contract marker).
|
|
82
|
+
* Any abstention still blocks "approve" (the outcome becomes at least
|
|
83
|
+
* revise-recommended); an abstention on a CRITICAL rule can never be silently
|
|
84
|
+
* approved and is reported first.
|
|
77
85
|
*
|
|
78
86
|
* EXIT CODE preserves the engine's honesty:
|
|
79
87
|
* >=1 critical OR revise-mandatory -> 1 (a violation IS wrong)
|
|
80
88
|
* 1..5 major violations (revise-recommended) -> 1 (hard violations exist)
|
|
81
|
-
* no hard violation,
|
|
89
|
+
* no hard violation, input unreadable somewhere -> 2 (not read = never a clean 0)
|
|
82
90
|
* approve -> 0
|
|
83
91
|
* We cross-check against the engine's computeExitCode and never print "approve"
|
|
84
92
|
* on an exit that means abstention.
|
|
@@ -113,7 +121,7 @@ export function verdict(t, results, severity = deriveSeverityMap()) {
|
|
|
113
121
|
exitCode = 1; // hard violations present — a caller must not read a clean 0
|
|
114
122
|
}
|
|
115
123
|
else if (abstentionTotal > 0) {
|
|
116
|
-
// no hard violation, but
|
|
124
|
+
// no hard violation, but some input could not be READ (critical abstention first)
|
|
117
125
|
outcome = "revise-recommended";
|
|
118
126
|
exitCode = 2;
|
|
119
127
|
}
|
|
@@ -125,7 +133,7 @@ export function verdict(t, results, severity = deriveSeverityMap()) {
|
|
|
125
133
|
const criticalNote = criticalViolations > 0
|
|
126
134
|
? `${criticalViolations} CRITICAL violation(s) — revise mandatory, NO bypass.`
|
|
127
135
|
: criticalAbstentions > 0
|
|
128
|
-
? `${criticalAbstentions} CRITICAL abstention(s) —
|
|
136
|
+
? `${criticalAbstentions} CRITICAL abstention(s) — not decided (each rule names why); approve is blocked.`
|
|
129
137
|
: "no critical violation or abstention.";
|
|
130
138
|
const summary = `VERDICT: ${outcome} (exit ${exitCode}) — ${criticalNote} ` +
|
|
131
139
|
`major violations=${majorViolations}, abstentions=${abstentionTotal} ` +
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { DetectorResult } from "./engine-types.js";
|
|
2
|
+
import type { MechanicalCount } from "./rule-class.js";
|
|
3
|
+
export interface ViolationCount {
|
|
4
|
+
/** the non-conforming count the verdict line prints. */
|
|
5
|
+
count: number;
|
|
6
|
+
/**
|
|
7
|
+
* per module (see moduleOf) — present for a site-based count, absent for a
|
|
8
|
+
* row- or finding-based one; the values always sum to `count`.
|
|
9
|
+
*/
|
|
10
|
+
modules?: Record<string, number>;
|
|
11
|
+
}
|
|
12
|
+
/** Attach `c` to `r` and return `r` (so a producer can `return withCount({...}, c)`). */
|
|
13
|
+
export declare function withCount<R extends DetectorResult>(r: R, c: ViolationCount): R;
|
|
14
|
+
export declare function countOf(r: DetectorResult): ViolationCount | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The module a site's file belongs to. A convex/ file is its basename without
|
|
17
|
+
* extension (`convex/tasks.ts` -> `tasks`, `convex/lib/auth.ts` -> `auth`). A file
|
|
18
|
+
* outside convex/ (R-53 also reads `mcp-server/src`) keeps its path without
|
|
19
|
+
* extension (`mcp-server/src/tools.ts` -> `mcp-server/src/tools`): it can never
|
|
20
|
+
* collide with a convex basename, and every site has a module, so the split
|
|
21
|
+
* always sums to the count.
|
|
22
|
+
*/
|
|
23
|
+
export declare function moduleOf(rel: string): string;
|
|
24
|
+
/** A site-based count: one item per site, split by module. */
|
|
25
|
+
export declare function siteCount(sites: readonly {
|
|
26
|
+
rel: string;
|
|
27
|
+
}[]): ViolationCount;
|
|
28
|
+
/** A row- or finding-based count: no file, so no module split. */
|
|
29
|
+
export declare function itemCount(n: number): ViolationCount;
|
|
30
|
+
/** Sum the counts of several non-conforming results of one rule (merged halves). */
|
|
31
|
+
export declare function sumCounts(parts: readonly (ViolationCount | undefined)[]): ViolationCount | undefined;
|
|
32
|
+
export interface MechanicalRuleCounts {
|
|
33
|
+
/** red mechanical rule id -> its non-conforming count; null when not certifiable. */
|
|
34
|
+
counts: Record<string, number> | null;
|
|
35
|
+
/** red mechanical rule id -> module -> count, for site-based rules. */
|
|
36
|
+
moduleCounts: Record<string, Record<string, number>> | null;
|
|
37
|
+
/** red mechanical rules with no attached count (the reason `counts` is null). */
|
|
38
|
+
uncounted: string[];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* One entry per rule in `mech.mechanicalViolations`: the sum of the attached
|
|
42
|
+
* counts of that rule's non-conforming results (the tally reduces a rule to its
|
|
43
|
+
* worst result; when several results of a rule are red, each one's items are
|
|
44
|
+
* printed and counted). Null when `mech.count` is null or any red rule is uncounted.
|
|
45
|
+
*/
|
|
46
|
+
export declare function mechanicalRuleCounts(results: readonly DetectorResult[], mech: MechanicalCount): MechanicalRuleCounts;
|
|
47
|
+
/** The human line printed under MECHANICAL VIOLATIONS. */
|
|
48
|
+
export declare function mechanicalRuleCountsLine(m: MechanicalRuleCounts): string;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const COUNTS = new WeakMap();
|
|
2
|
+
/** Attach `c` to `r` and return `r` (so a producer can `return withCount({...}, c)`). */
|
|
3
|
+
export function withCount(r, c) {
|
|
4
|
+
COUNTS.set(r, c);
|
|
5
|
+
return r;
|
|
6
|
+
}
|
|
7
|
+
export function countOf(r) {
|
|
8
|
+
return COUNTS.get(r);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* The module a site's file belongs to. A convex/ file is its basename without
|
|
12
|
+
* extension (`convex/tasks.ts` -> `tasks`, `convex/lib/auth.ts` -> `auth`). A file
|
|
13
|
+
* outside convex/ (R-53 also reads `mcp-server/src`) keeps its path without
|
|
14
|
+
* extension (`mcp-server/src/tools.ts` -> `mcp-server/src/tools`): it can never
|
|
15
|
+
* collide with a convex basename, and every site has a module, so the split
|
|
16
|
+
* always sums to the count.
|
|
17
|
+
*/
|
|
18
|
+
export function moduleOf(rel) {
|
|
19
|
+
const noExt = (p) => p.replace(/\.[^./]+$/, "");
|
|
20
|
+
if (/(^|\/)convex\//.test(rel))
|
|
21
|
+
return noExt(rel.split("/").pop() ?? rel);
|
|
22
|
+
return noExt(rel.replace(/^\.\//, ""));
|
|
23
|
+
}
|
|
24
|
+
/** A site-based count: one item per site, split by module. */
|
|
25
|
+
export function siteCount(sites) {
|
|
26
|
+
const modules = {};
|
|
27
|
+
for (const s of sites) {
|
|
28
|
+
const m = moduleOf(s.rel);
|
|
29
|
+
modules[m] = (modules[m] ?? 0) + 1;
|
|
30
|
+
}
|
|
31
|
+
return { count: sites.length, modules: sortKeys(modules) };
|
|
32
|
+
}
|
|
33
|
+
/** A row- or finding-based count: no file, so no module split. */
|
|
34
|
+
export function itemCount(n) {
|
|
35
|
+
return { count: n };
|
|
36
|
+
}
|
|
37
|
+
/** Sum the counts of several non-conforming results of one rule (merged halves). */
|
|
38
|
+
export function sumCounts(parts) {
|
|
39
|
+
if (parts.length === 0 || parts.some((p) => p === undefined))
|
|
40
|
+
return undefined;
|
|
41
|
+
let count = 0;
|
|
42
|
+
let modules = {};
|
|
43
|
+
for (const p of parts) {
|
|
44
|
+
count += p.count;
|
|
45
|
+
if (p.modules === undefined || modules === undefined)
|
|
46
|
+
modules = undefined;
|
|
47
|
+
else
|
|
48
|
+
for (const [k, v] of Object.entries(p.modules))
|
|
49
|
+
modules[k] = (modules[k] ?? 0) + v;
|
|
50
|
+
}
|
|
51
|
+
return modules === undefined
|
|
52
|
+
? { count }
|
|
53
|
+
: { count, modules: sortKeys(modules) };
|
|
54
|
+
}
|
|
55
|
+
function sortKeys(o) {
|
|
56
|
+
return Object.fromEntries(Object.entries(o).sort(([a], [b]) => a.localeCompare(b)));
|
|
57
|
+
}
|
|
58
|
+
const ruleOf = (r) => /^((?:R|CR)-\d+)\b/.exec(r.id)?.[1];
|
|
59
|
+
/**
|
|
60
|
+
* One entry per rule in `mech.mechanicalViolations`: the sum of the attached
|
|
61
|
+
* counts of that rule's non-conforming results (the tally reduces a rule to its
|
|
62
|
+
* worst result; when several results of a rule are red, each one's items are
|
|
63
|
+
* printed and counted). Null when `mech.count` is null or any red rule is uncounted.
|
|
64
|
+
*/
|
|
65
|
+
export function mechanicalRuleCounts(results, mech) {
|
|
66
|
+
if (mech.count === null)
|
|
67
|
+
return { counts: null, moduleCounts: null, uncounted: [] };
|
|
68
|
+
const counts = {};
|
|
69
|
+
const moduleCounts = {};
|
|
70
|
+
const uncounted = [];
|
|
71
|
+
for (const id of mech.mechanicalViolations) {
|
|
72
|
+
const red = results.filter((r) => ruleOf(r) === id && r.state === "non-conforming");
|
|
73
|
+
const c = sumCounts(red.map(countOf));
|
|
74
|
+
if (c === undefined) {
|
|
75
|
+
uncounted.push(id);
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
counts[id] = c.count;
|
|
79
|
+
if (c.modules !== undefined)
|
|
80
|
+
moduleCounts[id] = c.modules;
|
|
81
|
+
}
|
|
82
|
+
if (uncounted.length > 0)
|
|
83
|
+
return { counts: null, moduleCounts: null, uncounted };
|
|
84
|
+
return { counts, moduleCounts, uncounted };
|
|
85
|
+
}
|
|
86
|
+
/** The human line printed under MECHANICAL VIOLATIONS. */
|
|
87
|
+
export function mechanicalRuleCountsLine(m) {
|
|
88
|
+
if (m.counts === null)
|
|
89
|
+
return m.uncounted.length > 0
|
|
90
|
+
? `MECHANICAL RULE COUNTS: COULD-NOT-JUDGE — no count attached for ${m.uncounted.join(", ")} (the evidence file records null)`
|
|
91
|
+
: "MECHANICAL RULE COUNTS: COULD-NOT-JUDGE (the mechanical count is not certified; the evidence file records null)";
|
|
92
|
+
const entries = Object.entries(m.counts);
|
|
93
|
+
return `MECHANICAL RULE COUNTS: ${entries.length ? entries.map(([id, n]) => `${id} ${n}`).join(", ") : "none"}`;
|
|
94
|
+
}
|