instar 1.3.1157 → 1.3.1158
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-direct-url-log.js +72 -10
- 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.1158.md +30 -0
- package/upgrades/side-effects/url-log-lint-split-literal.md +131 -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.1158",
|
|
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": "9cbf312433ed10eafb3f33a245b9ba17546fdd534ce1c2caf3dcaf28157ee33c",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1158"
|
|
5
5
|
}
|
package/package.json
CHANGED
|
@@ -11,6 +11,26 @@
|
|
|
11
11
|
* Conservative by design: it flags the two concrete shapes we know leak, not
|
|
12
12
|
* every URL log. The redactUrl module + its tests are exempt.
|
|
13
13
|
*
|
|
14
|
+
* SCOPE, split by whether the match detects the prohibited FACT or a SPELLING
|
|
15
|
+
* of it (the distinction that decides which half is worth widening):
|
|
16
|
+
*
|
|
17
|
+
* - CREDENTIALED_URL_LITERAL is the FACT. A `user:pass@` inside a URL literal
|
|
18
|
+
* IS the leak, whatever it is called or where it is logged. So resolving a
|
|
19
|
+
* split literal is a real closure, not a bigger net: as of 2026-08-15 the
|
|
20
|
+
* line is scanned with adjacent string concatenations folded, because
|
|
21
|
+
* `"https://user:" + "tok@host"` leaked exactly as much as the one-piece
|
|
22
|
+
* form and was invisible. Folding joins only ADJACENT literals of the same
|
|
23
|
+
* quote style and invents no text.
|
|
24
|
+
*
|
|
25
|
+
* - RISKY_URL_VAR_LOG is a SPELLING. It matches five variable names logged
|
|
26
|
+
* through `console.*`. Renaming the variable (`originUrl`, `endpoint`) or
|
|
27
|
+
* using any other sink (`logger.info`) defeats it — both measured. Growing
|
|
28
|
+
* the name list would make the net finer while leaving the judgment inside
|
|
29
|
+
* the pattern, so it is deliberately NOT widened here. The right repair is
|
|
30
|
+
* to demote it from decider to candidate-gatherer and put the weighing
|
|
31
|
+
* downstream; that changes the check's authority and belongs in a spec, not
|
|
32
|
+
* in a regex edit.
|
|
33
|
+
*
|
|
14
34
|
* Exit 0 = clean. Exit 1 = at least one offending site (printed).
|
|
15
35
|
*/
|
|
16
36
|
|
|
@@ -29,6 +49,23 @@ const EXEMPT = [
|
|
|
29
49
|
/** A literal `scheme://user:pass@` in a string that is being logged. */
|
|
30
50
|
const CREDENTIALED_URL_LITERAL = /['"`][a-z][a-z0-9+.-]*:\/\/[^/@'"`\s]+:[^/@'"`\s]+@/i;
|
|
31
51
|
|
|
52
|
+
/**
|
|
53
|
+
* Fold `"a" + "b"` (adjacent string literals, same quote style) into `"ab"` so a
|
|
54
|
+
* credentialed URL split across a concatenation is scanned as the string it
|
|
55
|
+
* actually builds. Only literal+literal joins are folded — a variable operand
|
|
56
|
+
* ends the fold, so nothing is invented and no non-literal is assumed.
|
|
57
|
+
*/
|
|
58
|
+
export function collapseConcatenation(line) {
|
|
59
|
+
let out = line;
|
|
60
|
+
for (let i = 0; i < 8; i += 1) {
|
|
61
|
+
const next = out.replace(/(['"])((?:\\.|(?!\1)[^\\])*)\1\s*\+\s*(['"])((?:\\.|(?!\3)[^\\])*)\3/g,
|
|
62
|
+
(_m, q1, a, _q2, b) => `${q1}${a}${b}${q1}`);
|
|
63
|
+
if (next === out) break;
|
|
64
|
+
out = next;
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
32
69
|
/** console.* logging a variable named like a clone/remote URL without redactUrl on the same line. */
|
|
33
70
|
const RISKY_URL_VAR_LOG = /console\.(log|error|warn|info)\([^)]*\b(repoUrl|cloneUrl|remoteUrl|pushUrl|gitUrl)\b/;
|
|
34
71
|
|
|
@@ -45,14 +82,32 @@ function walk(dir, out = []) {
|
|
|
45
82
|
return out;
|
|
46
83
|
}
|
|
47
84
|
|
|
85
|
+
/**
|
|
86
|
+
* The scan, callable. Returns the offender list instead of exiting, so the
|
|
87
|
+
* behaviour can be unit-tested.
|
|
88
|
+
*
|
|
89
|
+
* This module previously exported nothing, so running the whole scan at module
|
|
90
|
+
* scope and calling process.exit() was harmless. Adding an export above makes
|
|
91
|
+
* that live: importing it to test the fold would run the repo scan and kill the
|
|
92
|
+
* test process the moment the repo had a real violation. Hence the
|
|
93
|
+
* direct-invocation guard at the bottom.
|
|
94
|
+
*/
|
|
95
|
+
// `srcDir`/`rootDir` default to this repo, so the shipped CLI behaviour is
|
|
96
|
+
// unchanged. They exist so the scanner can be driven over a throwaway tree in a
|
|
97
|
+
// test WITHOUT planting a probe file inside `src/` — planting one there both
|
|
98
|
+
// trips SourceTreeGuard (which refuses any delete inside the instar source
|
|
99
|
+
// tree) and is visible to every other test running at the same time.
|
|
100
|
+
export function scanForCredentialedUrlLogs(srcDir = SRC, rootDir = ROOT) {
|
|
48
101
|
const offenders = [];
|
|
49
|
-
for (const file of walk(
|
|
50
|
-
const rel = path.relative(
|
|
102
|
+
for (const file of walk(srcDir)) {
|
|
103
|
+
const rel = path.relative(rootDir, file);
|
|
51
104
|
if (EXEMPT.includes(rel)) continue;
|
|
52
105
|
const lines = fs.readFileSync(file, 'utf-8').split('\n');
|
|
53
106
|
lines.forEach((line, i) => {
|
|
54
107
|
const hasRedact = line.includes('redactUrl') || line.includes('redactUrlsInText');
|
|
55
|
-
|
|
108
|
+
// The FACT half is tested against the folded line; the SPELLING half is
|
|
109
|
+
// tested against the raw line exactly as before (unchanged behaviour).
|
|
110
|
+
if (CREDENTIALED_URL_LITERAL.test(collapseConcatenation(line))) {
|
|
56
111
|
offenders.push(`${rel}:${i + 1} credentialed-URL literal: ${line.trim().slice(0, 100)}`);
|
|
57
112
|
} else if (RISKY_URL_VAR_LOG.test(line) && !hasRedact) {
|
|
58
113
|
offenders.push(`${rel}:${i + 1} logs a clone/remote URL var without redactUrl(): ${line.trim().slice(0, 100)}`);
|
|
@@ -60,11 +115,18 @@ for (const file of walk(SRC)) {
|
|
|
60
115
|
});
|
|
61
116
|
}
|
|
62
117
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
118
|
+
return offenders;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Only scan + exit when RUN, never when imported (see the note above).
|
|
122
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
|
|
123
|
+
const offenders = scanForCredentialedUrlLogs();
|
|
124
|
+
if (offenders.length > 0) {
|
|
125
|
+
console.error('[lint-no-direct-url-log] credentialed-URL logging detected:');
|
|
126
|
+
for (const o of offenders) console.error(` - ${o}`);
|
|
127
|
+
console.error('\nRoute the URL through redactUrl()/redactUrlsInText() from src/core/redactUrl.ts before logging.');
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
console.log('[lint-no-direct-url-log] ✓ no credentialed-URL logging found');
|
|
131
|
+
process.exit(0);
|
|
68
132
|
}
|
|
69
|
-
console.log('[lint-no-direct-url-log] ✓ no credentialed-URL logging found');
|
|
70
|
-
process.exit(0);
|
|
@@ -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-15T04:14:15.394Z",
|
|
5
|
+
"instarVersion": "1.3.1158",
|
|
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.1158",
|
|
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": "9cbf312433ed10eafb3f33a245b9ba17546fdd534ce1c2caf3dcaf28157ee33c",
|
|
3
3
|
"registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
|
|
4
|
-
"packageVersion": "1.3.
|
|
4
|
+
"packageVersion": "1.3.1158"
|
|
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
|
+
`scripts/lint-no-direct-url-log.js` now folds adjacent string-literal
|
|
9
|
+
concatenations before testing its credentialed-URL pattern, so a
|
|
10
|
+
`scheme://user:pass@host` literal split across a `+` is detected as the string it
|
|
11
|
+
actually builds. The sibling variable-name pattern is unchanged.
|
|
12
|
+
|
|
13
|
+
The module also gains a direct-invocation guard: it now exports its scan, and
|
|
14
|
+
without the guard importing it would run the whole repo scan and `process.exit`.
|
|
15
|
+
|
|
16
|
+
## What to Tell Your User
|
|
17
|
+
|
|
18
|
+
None — internal change (no user-facing surface).
|
|
19
|
+
|
|
20
|
+
## Summary of New Capabilities
|
|
21
|
+
|
|
22
|
+
None — internal change (no user-facing surface).
|
|
23
|
+
|
|
24
|
+
## Evidence
|
|
25
|
+
|
|
26
|
+
- 3 new defect cases fail against the shipped fold behaviour; 13 controls pass
|
|
27
|
+
both ways. Source restored byte-identical after the mutation.
|
|
28
|
+
- Four anti-over-block fixtures return identical verdicts under the shipped and
|
|
29
|
+
fixed lint — zero new false positives, measured rather than argued.
|
|
30
|
+
- Real tree exit 0 before and after.
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# Side effects — url-log lint resolves split credentialed literals
|
|
2
|
+
|
|
3
|
+
## 1. Over-block
|
|
4
|
+
|
|
5
|
+
The dominant risk: this lint fails builds, so flagging correct code is more
|
|
6
|
+
expensive than missing a case (a noisy check gets switched off, and then it
|
|
7
|
+
guards nothing).
|
|
8
|
+
|
|
9
|
+
Bounded four ways:
|
|
10
|
+
- the fold joins only ADJACENT string literals of the SAME quote style;
|
|
11
|
+
- a variable operand ENDS the fold, so no runtime value is ever assumed;
|
|
12
|
+
- the fold is bounded to 8 passes and returns the input unchanged if it cannot
|
|
13
|
+
make progress;
|
|
14
|
+
- it can only ever JOIN existing literal text — a test pins that the folded line
|
|
15
|
+
is never longer than the input, so the fold cannot synthesise content.
|
|
16
|
+
|
|
17
|
+
Measured, not asserted: four anti-over-block fixtures were run against BOTH the
|
|
18
|
+
shipped lint and the fixed lint and returned identical verdicts. The real tree
|
|
19
|
+
lints clean before and after.
|
|
20
|
+
|
|
21
|
+
Note one verdict that looks like over-block and is not: a hardcoded
|
|
22
|
+
`https://user:tok@host` literal is flagged even when the log call redacts it.
|
|
23
|
+
That is pre-existing behaviour (the literal branch never consulted the redaction
|
|
24
|
+
check) and it is correct — a credential hardcoded in source is a leak regardless
|
|
25
|
+
of what happens at log time. Verified identical under the shipped lint.
|
|
26
|
+
|
|
27
|
+
## 2. Under-block — what this does NOT close
|
|
28
|
+
|
|
29
|
+
Stated by kind, because the two halves of this lint fail differently:
|
|
30
|
+
|
|
31
|
+
- **The variable-name half is untouched and still defeatable.** `RISKY_URL_VAR_LOG`
|
|
32
|
+
matches five names through `console.*`. A renamed variable (`originUrl`) and a
|
|
33
|
+
different sink (`logger.info`) both evade it — measured. This is deliberate:
|
|
34
|
+
that pattern matches a SPELLING correlated with the behaviour rather than the
|
|
35
|
+
behaviour, so widening the list makes a finer net and no more of a policy. The
|
|
36
|
+
correct repair is to demote it from decider to candidate-gatherer with the
|
|
37
|
+
weighing downstream, which changes the check's authority and belongs in a spec.
|
|
38
|
+
- Cross-line construction (a credentialed URL assembled over several statements)
|
|
39
|
+
is not resolved — that needs dataflow, not a line-scoped fold.
|
|
40
|
+
- A credentialed URL arriving from config, argv, or another module is invisible
|
|
41
|
+
to any source-text check.
|
|
42
|
+
|
|
43
|
+
## 3. Level-of-abstraction fit
|
|
44
|
+
|
|
45
|
+
Correct layer. A `user:pass@` inside a URL literal is an exact lexical fact about
|
|
46
|
+
our own source, which is what a deterministic source lint is for. The runtime
|
|
47
|
+
redaction funnel (`src/core/redactUrl.ts`) remains the authority for URLs whose
|
|
48
|
+
credentials only exist at runtime; this lint does not and cannot replace it.
|
|
49
|
+
|
|
50
|
+
## 4. Signal vs authority
|
|
51
|
+
|
|
52
|
+
Unchanged. The lint holds the same blocking authority it already had, over a
|
|
53
|
+
strictly more accurate view of the same prohibited fact. No new authority, no
|
|
54
|
+
new decision class, no runtime surface.
|
|
55
|
+
|
|
56
|
+
## 5. Interactions
|
|
57
|
+
|
|
58
|
+
None. No other check reads this one's output. The direct-invocation guard is
|
|
59
|
+
additive — it only changes behaviour on `import`, which nothing did before,
|
|
60
|
+
because the module exported nothing until now.
|
|
61
|
+
|
|
62
|
+
## 6. External surfaces
|
|
63
|
+
|
|
64
|
+
None. CI-only. No runtime code path, no API, no user-visible behaviour.
|
|
65
|
+
|
|
66
|
+
## 7. Multi-machine posture
|
|
67
|
+
|
|
68
|
+
Not applicable — machine-local by design. This is a build-time check over source
|
|
69
|
+
text in a checkout; it holds no state and replicates nothing.
|
|
70
|
+
|
|
71
|
+
## 8. Rollback cost
|
|
72
|
+
|
|
73
|
+
Revert the commit. The lint returns to its previous matching behaviour; nothing
|
|
74
|
+
persists and no state migrates.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Addendum — CI caught a guard conflict in the TEST, and the fix is a redesign not a workaround
|
|
79
|
+
|
|
80
|
+
**What CI found.** `Unit Tests shard 1/4` failed on node 20 AND node 22 (same shard both versions, so
|
|
81
|
+
not a flake): `SourceTreeGuardError: Refusing to run ... (requested dir:
|
|
82
|
+
.../src/core/__urlLogLintProbe.ts, resolved git root: ...)`. The test planted a probe file inside
|
|
83
|
+
`src/` so the real lint would scan it, then removed it through `SafeFsExecutor` — and SourceTreeGuard
|
|
84
|
+
refuses ANY delete inside the instar source tree (the 2026-04-22 incident class).
|
|
85
|
+
|
|
86
|
+
**Why it passed locally and failed in CI is the honest part:** I did not establish that, and I am not
|
|
87
|
+
guessing at it. What I did establish is that the test design was wrong on its own terms regardless of
|
|
88
|
+
which environment surfaces it.
|
|
89
|
+
|
|
90
|
+
**Two defects in that design, and the second is the one I had not considered:**
|
|
91
|
+
1. It performs a destructive operation inside the source tree — exactly what the guard exists to stop.
|
|
92
|
+
Routing it through the audited funnel satisfied the destructive-op lint and walked into a different
|
|
93
|
+
guard. Satisfying one guard is not evidence about another.
|
|
94
|
+
2. A probe file sitting at `src/core/__urlLogLintProbe.ts` is **visible to every other suite running at
|
|
95
|
+
the same time**. That is shared mutable state in a shared tree, and it would have been a latent
|
|
96
|
+
flake source for other people's tests, not just mine.
|
|
97
|
+
|
|
98
|
+
**The fix.** `scanForCredentialedUrlLogs()` now takes optional `srcDir`/`rootDir` that DEFAULT to this
|
|
99
|
+
repo, so the shipped CLI behaviour is unchanged. The test scans a throwaway temp tree instead. Nothing
|
|
100
|
+
is written into `src/` and nothing is deleted inside the source tree.
|
|
101
|
+
|
|
102
|
+
### Review answers for this addendum
|
|
103
|
+
|
|
104
|
+
1. **Over-block.** None new. The CLI path takes the defaults and is behaviourally identical — verified
|
|
105
|
+
by running it before and after the change (`exit 0` both times) and by the real-tree control below.
|
|
106
|
+
2. **Under-block.** One thing genuinely got WEAKER and I am naming it rather than letting it pass: the
|
|
107
|
+
defect cases now drive the exported scanner, so they no longer exercise the CLI's
|
|
108
|
+
offender-printing + `process.exit(1)` path end to end. A new test keeps the exit-code path covered
|
|
109
|
+
in the passing direction (`the shipped CLI still exits 0 on the clean tree`), but the failing
|
|
110
|
+
direction of the CLI wrapper is uncovered. That wrapper is eight lines and unchanged by this PR.
|
|
111
|
+
3. **New surface introduced.** The scanner now trusts a caller-supplied root. A caller could point it
|
|
112
|
+
at an empty directory and receive a clean verdict — but this is a lint's test seam, not a decision
|
|
113
|
+
authority, and the CLI never passes arguments. Named because "a scan over nothing reports clean" is
|
|
114
|
+
the exact defect I fixed in `lint-no-direct-destructive` earlier this window; here it is reachable
|
|
115
|
+
only by a caller that deliberately supplies the wrong root.
|
|
116
|
+
4. **Signal vs authority.** Unchanged — still a deterministic detector with a build-failing exit code.
|
|
117
|
+
5. **Interactions.** Removes an interaction rather than adding one: no more shared probe file inside
|
|
118
|
+
`src/` for concurrent suites to observe.
|
|
119
|
+
6. **Multi-machine posture.** Machine-local by design — a lint script and its unit test.
|
|
120
|
+
7. **Rollback cost.** Revert the commit; the parameters are additive with defaults.
|
|
121
|
+
|
|
122
|
+
### Evidence
|
|
123
|
+
|
|
124
|
+
- `tests/unit/url-log-lint-split-literal.test.ts` — **17/17 green** (16 before; +1 for the CLI exit path).
|
|
125
|
+
- **Negative control re-run after the redesign, because rewriting HOW a test drives its subject can
|
|
126
|
+
quietly turn it into a check that cannot fail:** removing the fold from the decision makes exactly
|
|
127
|
+
the **3 defect cases fail**, 14 pass both ways. Source restored byte-exact (sha match, 0 markers).
|
|
128
|
+
- Real-tree anti-over-block control is now STRONGER: it asserts `scanForCredentialedUrlLogs()` returns
|
|
129
|
+
`[]` under the real defaults, instead of asserting a temp tree with one inert line is clean.
|
|
130
|
+
- `tsc --noEmit` exit 0 (run via the real binary — `npx tsc` here is a shim that exits 0 without
|
|
131
|
+
typechecking). Full lint chain exit 0.
|