instar 1.3.1140 → 1.3.1142
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/dist/data/standards-guard-index.json +1 -1
- package/dist/data/standards-guard-index.meta.json +2 -2
- package/dist/data/standards-registry.meta.json +1 -1
- package/package.json +1 -1
- package/scripts/lint-no-unbounded-llm-spawn.js +23 -2
- package/scripts/lint-no-unfunneled-credential-write.js +90 -19
- package/src/data/builtin-manifest.json +2 -2
- package/src/data/standards-guard-index.json +1 -1
- package/src/data/standards-guard-index.meta.json +2 -2
- package/src/data/standards-registry.meta.json +1 -1
- package/upgrades/1.3.1141.md +30 -0
- package/upgrades/1.3.1142.md +32 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1142",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "588004e7496311e759c00e2ee9419b8385e05ca9cedff117ec3e44c9f78d73e2",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1142"
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -81,12 +81,29 @@ const EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.mjs', '.cjs']);
|
|
|
81
81
|
// Found by a peer-agent audit for checks defeatable by renaming. Resolve the
|
|
82
82
|
// local bindings FIRST, then match constructions under any of them.
|
|
83
83
|
|
|
84
|
-
/** Every local name a provider class is bound to in this file, plus namespace forms.
|
|
84
|
+
/** Every local name a provider class is bound to in this file, plus namespace forms.
|
|
85
|
+
*
|
|
86
|
+
* Widened 2026-08-14 after a peer-agent SABOTAGE pass against the first version
|
|
87
|
+
* found two SAME-FILE evasions the alias resolution missed:
|
|
88
|
+
* const C = ClaudeCliIntelligenceProvider; new C({}); // plain re-binding
|
|
89
|
+
* new Providers['ClaudeCliIntelligenceProvider']({}); // computed access
|
|
90
|
+
* The first is probably the easiest bypass of all to write. Re-binding is
|
|
91
|
+
* resolved to a FIXPOINT so chains (`const C = Cls; const D = C;`) close too. */
|
|
85
92
|
export function localProviderBindings(content, cls) {
|
|
86
93
|
const names = new Set([cls]);
|
|
87
94
|
// `import { Cls as Alias }` and `const { Cls: Alias } = await import(...)`
|
|
88
95
|
for (const m of content.matchAll(new RegExp(`\\b${cls}\\s+as\\s+([A-Za-z_$][\\w$]*)`, 'g'))) names.add(m[1]);
|
|
89
96
|
for (const m of content.matchAll(new RegExp(`\\b${cls}\\s*:\\s*([A-Za-z_$][\\w$]*)`, 'g'))) names.add(m[1]);
|
|
97
|
+
// `const|let|var Alias = <knownName>;` — plain re-binding, resolved to a
|
|
98
|
+
// fixpoint so a chain of re-bindings cannot walk out of the set.
|
|
99
|
+
for (let pass = 0; pass < 10; pass++) {
|
|
100
|
+
const before = names.size;
|
|
101
|
+
for (const known of [...names]) {
|
|
102
|
+
const re = new RegExp(`\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*${known}\\s*[;,\\n]`, 'g');
|
|
103
|
+
for (const m of content.matchAll(re)) names.add(m[1]);
|
|
104
|
+
}
|
|
105
|
+
if (names.size === before) break;
|
|
106
|
+
}
|
|
90
107
|
return [...names];
|
|
91
108
|
}
|
|
92
109
|
|
|
@@ -108,7 +125,11 @@ export function findProviderConstructions(content, classes = PROVIDER_CLASSES) {
|
|
|
108
125
|
const bare = names.some((n) => new RegExp(`\\bnew\\s+${n}\\s*\\(`).test(lines[i]));
|
|
109
126
|
// `new <ns>.<Cls>(` — a namespace import the bare form cannot see.
|
|
110
127
|
const viaNs = new RegExp(`\\bnew\\s+[A-Za-z_$][\\w$]*\\.${cls}\\s*\\(`).test(lines[i]);
|
|
111
|
-
|
|
128
|
+
// `new <ns>['<Cls>'](` — computed access; found by sabotage 2026-08-14.
|
|
129
|
+
const viaComputed = new RegExp(
|
|
130
|
+
`\\bnew\\s+[A-Za-z_$][\\w$]*\\s*\\[\\s*['"\`]${cls}['"\`]\\s*\\]\\s*\\(`,
|
|
131
|
+
).test(lines[i]);
|
|
132
|
+
if (bare || viaNs || viaComputed) { hits.push({ line: i + 1, cls }); break; }
|
|
112
133
|
}
|
|
113
134
|
}
|
|
114
135
|
return hits;
|
|
@@ -86,6 +86,83 @@ const PATTERNS = [
|
|
|
86
86
|
// services) are never false-positived.
|
|
87
87
|
const RAW_KEYCHAIN_WRITE = /add-generic-password/;
|
|
88
88
|
|
|
89
|
+
// ── Evasion-resistant detection (2026-08-14) ─────────────────────────────
|
|
90
|
+
// A peer-agent audit classified this check DEFEATABLE by ordinary renaming,
|
|
91
|
+
// and three bypasses were then reproduced against it:
|
|
92
|
+
//
|
|
93
|
+
// const SVC = 'Claude Code' + '-credentials'; // concatenated service
|
|
94
|
+
// execFileSync('security', ['add-generic-password', '-s', SVC]);
|
|
95
|
+
// const store = defaultCredentialStore; store.write(p); // re-bound receiver
|
|
96
|
+
// provider['writeCredentials'](p); // computed access
|
|
97
|
+
//
|
|
98
|
+
// The first is the sharpest: the raw-keychain rule is GATED on the literal
|
|
99
|
+
// service string appearing in the file, so splitting it across a concatenation
|
|
100
|
+
// switches the whole rule off. That gate exists to avoid false-positiving the
|
|
101
|
+
// other, distinct-service vaults, so it is narrowed rather than removed.
|
|
102
|
+
|
|
103
|
+
/** Collapse simple adjacent string concatenation so `'A' + 'B'` reads as `AB`. */
|
|
104
|
+
export function collapseConcatenation(content) {
|
|
105
|
+
// `'A' + 'B'` / "A" + "B" / mixed quotes → `AB`. Repeated to fold chains.
|
|
106
|
+
let out = content;
|
|
107
|
+
for (let i = 0; i < 5; i++) {
|
|
108
|
+
const next = out.replace(/(['"])\s*\+\s*(['"])/g, '');
|
|
109
|
+
if (next === out) break;
|
|
110
|
+
out = next;
|
|
111
|
+
}
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Local names bound to `defaultCredentialStore`, resolved to a fixpoint. */
|
|
116
|
+
export function credentialStoreBindings(content) {
|
|
117
|
+
const names = new Set(['defaultCredentialStore']);
|
|
118
|
+
for (let pass = 0; pass < 10; pass++) {
|
|
119
|
+
const before = names.size;
|
|
120
|
+
for (const known of [...names]) {
|
|
121
|
+
const re = new RegExp(`\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*=\\s*${known}\\s*[;,\\n]`, 'g');
|
|
122
|
+
for (const m of content.matchAll(re)) names.add(m[1]);
|
|
123
|
+
}
|
|
124
|
+
if (names.size === before) break;
|
|
125
|
+
}
|
|
126
|
+
return [...names];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Violations in `content`, as { line, msg }. Exported so the rules can be
|
|
131
|
+
* driven with fixtures rather than only end-to-end over the tree.
|
|
132
|
+
*/
|
|
133
|
+
export function findCredentialWriteViolations(content) {
|
|
134
|
+
const hits = [];
|
|
135
|
+
// Concatenation-tolerant: a split service literal no longer disarms the rule.
|
|
136
|
+
const targetsGuardedService = collapseConcatenation(content).includes(GUARDED_SERVICE);
|
|
137
|
+
const storeNames = credentialStoreBindings(content);
|
|
138
|
+
const lines = content.split('\n');
|
|
139
|
+
for (let i = 0; i < lines.length; i++) {
|
|
140
|
+
const line = lines[i];
|
|
141
|
+
if (isCommentLine(line)) continue;
|
|
142
|
+
// Re-bound receiver + computed access on the store write.
|
|
143
|
+
for (const n of storeNames) {
|
|
144
|
+
if (new RegExp(`\\b${n}\\s*\\.\\s*write\\s*\\(`).test(line)
|
|
145
|
+
|| new RegExp(`\\b${n}\\s*\\[\\s*['"\`]write['"\`]\\s*\\]\\s*\\(`).test(line)) {
|
|
146
|
+
hits.push({ line: i + 1, msg: PATTERNS[0].msg });
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// `.writeCredentials(` and its computed form.
|
|
151
|
+
if (/\.writeCredentials\s*\(/.test(line)
|
|
152
|
+
|| /\[\s*['"`]writeCredentials['"`]\s*\]\s*\(/.test(line)) {
|
|
153
|
+
hits.push({ line: i + 1, msg: PATTERNS[1].msg });
|
|
154
|
+
}
|
|
155
|
+
if (targetsGuardedService && RAW_KEYCHAIN_WRITE.test(line)) {
|
|
156
|
+
hits.push({
|
|
157
|
+
line: i + 1,
|
|
158
|
+
msg: `raw 'add-generic-password' to the ${GUARDED_SERVICE} service outside the funnel. `
|
|
159
|
+
+ `Route the write through CredentialWriteFunnel.withSlotLock, or add an allowlist entry here with a justification.`,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return hits;
|
|
164
|
+
}
|
|
165
|
+
|
|
89
166
|
function isCommentLine(line) {
|
|
90
167
|
const t = line.trim();
|
|
91
168
|
return t.startsWith('//') || t.startsWith('*') || t.startsWith('/*') || t.startsWith('#');
|
|
@@ -118,6 +195,15 @@ function listFiles() {
|
|
|
118
195
|
return files;
|
|
119
196
|
}
|
|
120
197
|
|
|
198
|
+
// ── CLI body ─────────────────────────────────────────────────────────────
|
|
199
|
+
// Guarded so the exported detector can be imported by tests WITHOUT running the
|
|
200
|
+
// scan: this module calls process.exit(1) on a violation, so an unguarded
|
|
201
|
+
// import would kill any test run the moment the repo had one. Same pattern as
|
|
202
|
+
// scripts/eli16-pr-description-check.mjs and lint-no-unbounded-llm-spawn.js.
|
|
203
|
+
const invokedDirectly =
|
|
204
|
+
process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href;
|
|
205
|
+
|
|
206
|
+
if (invokedDirectly) {
|
|
121
207
|
let violations = 0;
|
|
122
208
|
for (const rel of listFiles()) {
|
|
123
209
|
const normalized = rel.split(path.sep).join('/');
|
|
@@ -130,25 +216,9 @@ for (const rel of listFiles()) {
|
|
|
130
216
|
} catch {
|
|
131
217
|
continue;
|
|
132
218
|
}
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const line = lines[i];
|
|
137
|
-
if (isCommentLine(line)) continue;
|
|
138
|
-
for (const { re, msg } of PATTERNS) {
|
|
139
|
-
if (re.test(line)) {
|
|
140
|
-
console.error(`${normalized}:${i + 1} — ${msg}`);
|
|
141
|
-
violations++;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
if (fileTargetsGuardedService && RAW_KEYCHAIN_WRITE.test(line)) {
|
|
145
|
-
console.error(
|
|
146
|
-
`${normalized}:${i + 1} — raw 'add-generic-password' to the ${GUARDED_SERVICE} service outside ` +
|
|
147
|
-
`the funnel. Route the write through CredentialWriteFunnel.withSlotLock, or add an allowlist entry ` +
|
|
148
|
-
`here with a justification.`,
|
|
149
|
-
);
|
|
150
|
-
violations++;
|
|
151
|
-
}
|
|
219
|
+
for (const hit of findCredentialWriteViolations(content)) {
|
|
220
|
+
console.error(`${normalized}:${hit.line} — ${hit.msg}`);
|
|
221
|
+
violations++;
|
|
152
222
|
}
|
|
153
223
|
}
|
|
154
224
|
|
|
@@ -160,3 +230,4 @@ if (violations > 0) {
|
|
|
160
230
|
process.exit(1);
|
|
161
231
|
}
|
|
162
232
|
console.log('lint-no-unfunneled-credential-write: clean');
|
|
233
|
+
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-08-
|
|
5
|
-
"instarVersion": "1.3.
|
|
4
|
+
"generatedAt": "2026-08-14T20:03:30.012Z",
|
|
5
|
+
"instarVersion": "1.3.1142",
|
|
6
6
|
"entryCount": 202,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"generatedFrom": "source-tree",
|
|
4
4
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
5
|
-
"packageVersion": "1.3.
|
|
5
|
+
"packageVersion": "1.3.1142",
|
|
6
6
|
"guards": [
|
|
7
7
|
{
|
|
8
8
|
"ref": "docs/audits/phase-b/f10-triage.md",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"sha256": "
|
|
2
|
+
"sha256": "588004e7496311e759c00e2ee9419b8385e05ca9cedff117ec3e44c9f78d73e2",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1142"
|
|
5
5
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
After hardening the runaway-process guard against renamed imports this morning, a second agent was asked to break it — and got past it twice more.
|
|
9
|
+
|
|
10
|
+
- **Assigning the class to a variable and building from the variable** slipped through. That is probably the simplest bypass anyone could write, and it was not anticipated.
|
|
11
|
+
- **Reaching the class out of a module with bracket notation** instead of a dot also slipped through.
|
|
12
|
+
- **Both are now recognised.** Variable assignments are followed to a fixed point, so passing the class along several times cannot walk out of the set.
|
|
13
|
+
- **The written statement of what the guard still misses has been corrected.** It named one kind of gap; there were three. Two of them were inside the ground the earlier fix implied was covered, so leaving that statement unchanged would have left a claim on record narrower than the truth.
|
|
14
|
+
- **No new work.** The same constructions are forbidden as before, the codebase passes cleanly before and after.
|
|
15
|
+
|
|
16
|
+
## What to Tell Your User
|
|
17
|
+
|
|
18
|
+
This morning's fix made the guard understand renamed imports. It was then attacked on purpose, and two more ways past turned up — both of them ordinary code, both inside a single file, which is exactly what that fix claimed to have handled.
|
|
19
|
+
|
|
20
|
+
Those are closed. Three ways past remain, all involving a class passed onward through several modules; closing those needs the check to follow symbols across files, which it does not do. That limit is now written down and pinned by a test, so nobody later mistakes it for an oversight.
|
|
21
|
+
|
|
22
|
+
Worth noting how this was found: not by re-reading the fix, but by asking someone else to break it and telling them plainly that finding a hole would be more useful than a clean report. A review that can only confirm is not a review.
|
|
23
|
+
|
|
24
|
+
## Summary of New Capabilities
|
|
25
|
+
|
|
26
|
+
None. This widens what an existing check can see and corrects a written claim about its limits. No new command, route, setting, or rule.
|
|
27
|
+
|
|
28
|
+
## Evidence
|
|
29
|
+
|
|
30
|
+
The two bypasses are the reviewer's own snippets, reproduced verbatim as tests. Proven in both directions: reverting both additions fails four tests while thirteen still pass — those thirteen including all six deliberate opposite-direction controls and a test that pins the remaining cross-module gap as still open. Among the controls is a new one specifically for this change: assigning an unrelated symbol to a variable and building from it is not flagged, because a rule that treated every variable as suspicious would pass every bypass test and fail on ordinary code. The real codebase passes cleanly before and after; source restored byte-identical after the check.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The rule that stops secrets being written outside the approved path could be switched off by writing one name in two halves.
|
|
9
|
+
|
|
10
|
+
- **The raw-keychain part of the check only arms itself when it sees a particular service name written out in full.** That narrowing is deliberate — it keeps the other, unrelated keychain stores from being flagged — but it means splitting the name across a join turned the entire rule off.
|
|
11
|
+
- **Two smaller ways past existed too**: assign the credential store to a variable and write through the variable, or reach the write method with bracket notation instead of a dot.
|
|
12
|
+
- **All three were confirmed getting past the check before this change**, with a known-caught example passing in the same run to prove the probe worked.
|
|
13
|
+
- **All three are now closed.** Split strings are joined before the check decides whether to arm, variables holding the store are followed to a fixed point, and bracket access is recognised.
|
|
14
|
+
- **Nothing new is forbidden.** The same writes are disallowed as before; more of them are visible, and the codebase passes cleanly before and after.
|
|
15
|
+
|
|
16
|
+
## What to Tell Your User
|
|
17
|
+
|
|
18
|
+
Every write of the stored Claude credential is meant to go through a single controlled path, and a check enforces it. To avoid complaining about unrelated keychain entries, part of that check only switched on when it recognised a specific name spelled out in full — so spelling it in two pieces made the check ignore the file completely.
|
|
19
|
+
|
|
20
|
+
Nothing was actually being written unsafely; the hole was in the guard. It is closed, along with two smaller ones.
|
|
21
|
+
|
|
22
|
+
The care here went into the opposite risk. Widening a rule that blocks work can flood people with false complaints about correct code, and the quickest way to silence a noisy check is to switch it off. So five deliberate tests confirm the things that must *not* be flagged still are not, and the whole codebase passes cleanly before and after.
|
|
23
|
+
|
|
24
|
+
## Summary of New Capabilities
|
|
25
|
+
|
|
26
|
+
None. This widens what an existing check can see. No new command, route, setting, or rule.
|
|
27
|
+
|
|
28
|
+
## Evidence
|
|
29
|
+
|
|
30
|
+
The three bypasses were reproduced against the shipped check before any change, alongside a known-caught example in the same run to prove the probe could detect anything at all. Proven in both directions: restored to the old behaviour, seven tests fail and seven pass — and the seven that pass are exactly the five opposite-direction controls plus the plain cases, which is what makes them guards rather than echoes. Those controls check that a different keychain service is not flagged, that a raw keychain write with no reference to the guarded service is not flagged, that an unrelated store's write is not flagged, that an unrelated variable assignment is not absorbed, and that comments are not violations. The real codebase passes cleanly before and after, and the source was restored byte-identical after the check.
|
|
31
|
+
|
|
32
|
+
Remaining gaps are named rather than implied: a service name built through a template with a variable in it, or imported as a constant from another file, still disarms the rule — both need value resolution this check does not do.
|