candor-ts 0.7.7 → 0.8.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/package.json +1 -1
- package/policy.mjs +10 -5
- package/query.mjs +1 -1
- package/scan.mjs +23 -13
package/package.json
CHANGED
package/policy.mjs
CHANGED
|
@@ -109,14 +109,19 @@ export function literalAllowed(effect, reached, values) {
|
|
|
109
109
|
* transitive inferred; AS-EFF-008 allowlists over the transitive literal surfaces, the no-visible-
|
|
110
110
|
* literal case flagged as uncertifiable; AS-EFF-009 forbid by reachability). One line per violation.
|
|
111
111
|
*/
|
|
112
|
+
// Each violation is a STRUCTURED record { rule, fn, effects, detail } (candor-spec §3.3 ⟨0.8⟩): `effects`
|
|
113
|
+
// is the specific denied/allowed effect set the violation concerns ([] for the 009 layer-flow, which has
|
|
114
|
+
// no single effect); `detail` is the message BODY (no `[AS-EFF-00x]` prefix — the rule carries the code).
|
|
115
|
+
// The console gate renders `[${rule}] ${detail}`; --gate-json emits the records verbatim.
|
|
112
116
|
export function evaluatePolicy(pol, functions, callgraph, incomplete = new Map()) {
|
|
113
117
|
const out = [];
|
|
114
118
|
const surfaces = { Net: "hosts", Exec: "cmds", Fs: "paths", Db: "tables" };
|
|
119
|
+
const push = (rule, fn, effects, detail) => out.push({ rule, fn, effects, detail });
|
|
115
120
|
for (const f of functions) {
|
|
116
121
|
for (const r of pol.deny) {
|
|
117
122
|
if (r.scope && !scopeMatches(f.fn, r.scope)) continue;
|
|
118
123
|
const hits = r.effects.length === 0 ? f.inferred : f.inferred.filter((e) => r.effects.includes(e));
|
|
119
|
-
if (hits.length)
|
|
124
|
+
if (hits.length) push("AS-EFF-006", f.fn, hits, `\`${f.fn}\` performs { ${hits.join(", ")} }, forbidden by policy: \`${r.raw}\``);
|
|
120
125
|
}
|
|
121
126
|
for (const r of pol.allow) {
|
|
122
127
|
if (r.scope && !scopeMatches(f.fn, r.scope)) continue;
|
|
@@ -127,14 +132,14 @@ export function evaluatePolicy(pol, functions, callgraph, incomplete = new Map()
|
|
|
127
132
|
// invisible forbidden endpoint (the masking evasion). Matches candor-java 0.5.29 / candor-rust.
|
|
128
133
|
const surfaceIncomplete = incomplete.get(f.fn)?.has(r.effect);
|
|
129
134
|
if (reached.length === 0 || surfaceIncomplete) {
|
|
130
|
-
|
|
135
|
+
push("AS-EFF-008", f.fn, [r.effect], `\`${f.fn}\` performs ${r.effect} with no visible literal — the surface cannot be certified: \`${r.raw}\``);
|
|
131
136
|
} else {
|
|
132
137
|
const bad = reached.filter((v) => !literalAllowed(r.effect, v, r.values));
|
|
133
|
-
if (bad.length)
|
|
138
|
+
if (bad.length) push("AS-EFF-008", f.fn, [r.effect], `\`${f.fn}\` reaches { ${bad.join(", ")} } outside the allowlist: \`${r.raw}\``);
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
}
|
|
137
|
-
// AS-EFF-009: forbid A -> B by reachability over the callgraph.
|
|
142
|
+
// AS-EFF-009: forbid A -> B by reachability over the callgraph. No single effect → effects: [].
|
|
138
143
|
for (const r of pol.forbid) {
|
|
139
144
|
for (const fn of Object.keys(callgraph)) {
|
|
140
145
|
if (!scopeMatches(fn, r.from)) continue;
|
|
@@ -148,7 +153,7 @@ export function evaluatePolicy(pol, functions, callgraph, incomplete = new Map()
|
|
|
148
153
|
queue.push(c);
|
|
149
154
|
}
|
|
150
155
|
}
|
|
151
|
-
if (hit)
|
|
156
|
+
if (hit) push("AS-EFF-009", fn, [], `\`${fn}\` reaches into a forbidden layer (via \`${hit}\`), violating policy: \`${r.raw}\``);
|
|
152
157
|
}
|
|
153
158
|
}
|
|
154
159
|
return out;
|
package/query.mjs
CHANGED
|
@@ -37,7 +37,7 @@ const emit = (v) => console.log(JSON.stringify(v, null, 1));
|
|
|
37
37
|
// package.json; SPEC_VERSION is the spec contract this build speaks. Reused, never re-littered.
|
|
38
38
|
const QUERY_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
39
39
|
const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(QUERY_DIR, "package.json"), "utf8")).version;
|
|
40
|
-
const SPEC_VERSION = "0.
|
|
40
|
+
const SPEC_VERSION = "0.8";
|
|
41
41
|
|
|
42
42
|
// The full subcommand catalogue — name + one-line description (derived from the per-subcommand
|
|
43
43
|
// comments + the module-doc header). The single source for the --help list AND the no-arg/unknown
|
package/scan.mjs
CHANGED
|
@@ -36,7 +36,7 @@ const ENGINE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
|
36
36
|
// literal stamped into the envelope's `spec` field, so the doc lines and the report can never drift.
|
|
37
37
|
// Reused, never re-littered.
|
|
38
38
|
const PKG_VERSION = JSON.parse(fs.readFileSync(path.join(ENGINE_DIR, "package.json"), "utf8")).version;
|
|
39
|
-
const SPEC_VERSION = "0.
|
|
39
|
+
const SPEC_VERSION = "0.8";
|
|
40
40
|
|
|
41
41
|
// --version: a print-and-exit MODE, handled before the main arg walk so it never depends on a target.
|
|
42
42
|
// Fully OFFLINE — candor never phones home. Staying current is the AGENT's job: read the installed
|
|
@@ -52,13 +52,14 @@ if (process.argv.includes("--version") || process.argv.includes("-V")) {
|
|
|
52
52
|
if (process.argv.includes("-h") || process.argv.includes("--help")) {
|
|
53
53
|
console.log(`candor-ts ${PKG_VERSION} — TypeScript/JavaScript effect scanner (candor-spec ${SPEC_VERSION})
|
|
54
54
|
|
|
55
|
-
USAGE: candor-ts <dir | file.ts | tsconfig.json> [--out <prefix>] [--json] [--policy <file>] [--allow-js] [--agents] [--version]
|
|
55
|
+
USAGE: candor-ts <dir | file.ts | tsconfig.json> [--out <prefix>] [--json] [--policy <file>] [--gate-json <file>] [--allow-js] [--agents] [--version]
|
|
56
56
|
|
|
57
57
|
<target> a dir, a .ts file, or a tsconfig.json to scan
|
|
58
58
|
--out <prefix> write the report to <prefix>.json + <prefix>.callgraph.json
|
|
59
59
|
--json print the report as JSON to stdout (instead of writing files)
|
|
60
60
|
--policy <file> enforce a policy file (deny/pure/allow/forbid, candor-spec §6.2) — exit 1 on a
|
|
61
61
|
violation, 2 if unreadable; honours $CANDOR_POLICY when the flag is absent
|
|
62
|
+
--gate-json <f> write the structured gate verdict { spec, ok, violations } as JSON (candor-spec §3.3)
|
|
62
63
|
--allow-js also scan plain JS/Node (.js/.mjs/.cjs), not just TypeScript
|
|
63
64
|
--agents print the agent contract for this build (AGENTS.md)
|
|
64
65
|
-V, --version print the build and spec version (offline)
|
|
@@ -73,18 +74,18 @@ See https://github.com/tombaldwin/candor`);
|
|
|
73
74
|
// missing/flag-shaped value; an unknown flag fails; flags may precede the target. `--agents` is a
|
|
74
75
|
// flag (a print-and-exit MODE) — it must NOT fire when it is the VALUE of --out/--policy, which the
|
|
75
76
|
// value-consuming skip handles, nor produce a "lying unknown flag" error for a real flag given first.
|
|
76
|
-
const usage = "usage: candor-ts <dir | file.ts | tsconfig.json> [--out <prefix>] [--json] [--policy <file>] [--allow-js] [--agents] [--version] [--help]";
|
|
77
|
+
const usage = "usage: candor-ts <dir | file.ts | tsconfig.json> [--out <prefix>] [--json] [--policy <file>] [--gate-json <file>] [--allow-js] [--agents] [--version] [--help]";
|
|
77
78
|
const argv = process.argv.slice(2);
|
|
78
|
-
let target = null, outPrefix = null, policyPath = process.env.CANDOR_POLICY ?? null, allowJs = false, wantAgents = false, wantJson = false;
|
|
79
|
+
let target = null, outPrefix = null, policyPath = process.env.CANDOR_POLICY ?? null, gateJsonPath = null, allowJs = false, wantAgents = false, wantJson = false;
|
|
79
80
|
for (let i = 0; i < argv.length; i++) {
|
|
80
81
|
const a = argv[i];
|
|
81
82
|
if (a === "--agents") wantAgents = true;
|
|
82
83
|
else if (a === "--json") wantJson = true;
|
|
83
84
|
else if (a === "--allow-js") allowJs = true;
|
|
84
|
-
else if (a === "--out" || a === "--policy") {
|
|
85
|
+
else if (a === "--out" || a === "--policy" || a === "--gate-json") {
|
|
85
86
|
const v = argv[i + 1];
|
|
86
87
|
if (v === undefined || v.startsWith("--")) { console.error(`candor-ts: ${a} requires a value (${usage})`); process.exit(2); }
|
|
87
|
-
if (a === "--out") outPrefix = v; else policyPath = v;
|
|
88
|
+
if (a === "--out") outPrefix = v; else if (a === "--policy") policyPath = v; else gateJsonPath = v;
|
|
88
89
|
i++;
|
|
89
90
|
}
|
|
90
91
|
// Any leading-dash token that isn't a recognized flag is an unknown flag — NOT a positional target
|
|
@@ -2074,6 +2075,7 @@ if (unlistedSeen.size > 0) {
|
|
|
2074
2075
|
}
|
|
2075
2076
|
|
|
2076
2077
|
// ---- the standing §6.2 gate (--policy / CANDOR_POLICY) --------------------------------------------
|
|
2078
|
+
let gateViolations = [];
|
|
2077
2079
|
if (policyPath) {
|
|
2078
2080
|
let text;
|
|
2079
2081
|
try {
|
|
@@ -2087,14 +2089,22 @@ if (policyPath) {
|
|
|
2087
2089
|
// java/rust engines (not a report field) — passed to the gate so an incomplete surface fails closed.
|
|
2088
2090
|
const incompleteMap = new Map();
|
|
2089
2091
|
for (const [name, rec] of fns) if (rec.incomplete.size) incompleteMap.set(name, rec.incomplete);
|
|
2090
|
-
|
|
2092
|
+
gateViolations = evaluatePolicy(parsePolicy(text), functions, cg, incompleteMap);
|
|
2091
2093
|
// In --json mode stdout is the §2 envelope and must stay pure JSON — route the gate's
|
|
2092
2094
|
// [AS-EFF-…] violation lines to stderr so a `candor-ts --json --policy … | jq` pipe never breaks.
|
|
2093
2095
|
const emitViolation = wantJson ? (l) => console.error(l) : (l) => console.log(l);
|
|
2094
|
-
for (const
|
|
2095
|
-
|
|
2096
|
-
|
|
2097
|
-
|
|
2098
|
-
|
|
2099
|
-
|
|
2096
|
+
for (const x of gateViolations) emitViolation(`[${x.rule}] ${x.detail}`);
|
|
2097
|
+
}
|
|
2098
|
+
// --gate-json ⟨0.8⟩: the structured gate verdict { spec, ok, violations:[{rule,fn,effects,detail}] }, from
|
|
2099
|
+
// the SAME gateViolations that set the exit code (so it can't disagree). Written whenever the flag is set —
|
|
2100
|
+
// ok:true,[] when no gate is configured. Must precede the exit(1) below.
|
|
2101
|
+
if (gateJsonPath) {
|
|
2102
|
+
const verdict = JSON.stringify({ spec: SPEC_VERSION, ok: gateViolations.length === 0, violations: gateViolations }, null, 1);
|
|
2103
|
+
if (gateJsonPath === "-") console.log(verdict);
|
|
2104
|
+
else writeAtomic(gateJsonPath, verdict + "\n");
|
|
2105
|
+
}
|
|
2106
|
+
if (policyPath && gateViolations.length) {
|
|
2107
|
+
console.error(`candor-ts: ${gateViolations.length} policy violation(s)`);
|
|
2108
|
+
process.exit(1);
|
|
2100
2109
|
}
|
|
2110
|
+
if (policyPath) console.error("candor-ts: policy ✓");
|