@cosmicdrift/kumiko-guards 0.356.0 → 0.358.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 +2 -2
- package/src/_lib/bun-lock-packages.ts +5 -5
- package/src/_lib/guard-kit.ts +180 -131
- package/src/_lib/qn.ts +9 -9
- package/src/_lib/roots.ts +4 -3
- package/src/_lib/security-baseline.ts +193 -116
- package/src/_lib/sql-inventory.ts +34 -32
- package/src/changes.json +6 -0
- package/src/check-as-casts.ts +196 -180
- package/src/check-complexity.ts +68 -69
- package/src/check-predicates.ts +93 -67
- package/src/check-secret-literals.ts +49 -38
- package/src/check-single-runtime-instance.ts +1 -1
- package/src/cli.ts +30 -17
- package/src/guard-access-denied-test.ts +49 -23
- package/src/guard-app-feature-structure.ts +92 -73
- package/src/guard-broker-subscribe.ts +17 -17
- package/src/guard-comment-lang.ts +126 -54
- package/src/guard-direct-entity-writes.ts +58 -45
- package/src/guard-escape-hatch-declared.ts +26 -23
- package/src/guard-html-escape.ts +112 -86
- package/src/guard-i18n-keys.ts +39 -36
- package/src/guard-i18n-locale-mount.ts +47 -40
- package/src/guard-i18n-ui-strings.ts +108 -92
- package/src/guard-lib-test-coverage.ts +37 -37
- package/src/guard-loadall-events.ts +17 -17
- package/src/guard-no-framed-extension-sections.ts +4 -4
- package/src/guard-no-temporal-polyfill-import.ts +19 -1
- package/src/guard-pii-annotations.ts +16 -16
- package/src/guard-primitives-discipline.ts +61 -65
- package/src/guard-screen-conventions.ts +2 -0
- package/src/guard-table-ddl.ts +56 -62
- package/src/guard-tenant-escalation.ts +42 -34
- package/src/guard-test-stack-drift.ts +6 -6
- package/src/guard-test-timeouts.ts +5 -4
- package/src/guard-text-field-stance.ts +22 -22
- package/src/guard-thin-wrappers.ts +42 -45
- package/src/guard-write-handler-qns.ts +29 -29
- package/src/runtime-isolation-classify.ts +4 -3
|
@@ -56,22 +56,42 @@ function isFindingsShape(value: unknown): value is SecurityBaseline["findings"]
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
type BaselineCore = Pick<SecurityBaseline, "format" | "repo" | "generated" | "total" | "findings">;
|
|
60
|
+
|
|
61
|
+
function hasBaselineCore(raw: object): raw is BaselineCore {
|
|
62
|
+
return (
|
|
63
|
+
"format" in raw &&
|
|
64
|
+
raw.format === SECURITY_BASELINE_FORMAT &&
|
|
65
|
+
"repo" in raw &&
|
|
66
|
+
typeof raw.repo === "string" &&
|
|
67
|
+
"generated" in raw &&
|
|
68
|
+
typeof raw.generated === "string" &&
|
|
69
|
+
"total" in raw &&
|
|
70
|
+
typeof raw.total === "number" &&
|
|
71
|
+
"findings" in raw &&
|
|
72
|
+
isFindingsShape(raw.findings)
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// {} is allowed (guard has no current findings); any entry means it isn't done migrating yet.
|
|
77
|
+
function isValidHardFail(
|
|
78
|
+
hardFail: unknown,
|
|
79
|
+
findings: SecurityBaseline["findings"],
|
|
80
|
+
): hardFail is readonly string[] {
|
|
81
|
+
if (!isStringArrayNoDuplicates(hardFail)) return false;
|
|
82
|
+
return hardFail.every((guardName) => {
|
|
83
|
+
const perFile = findings[guardName];
|
|
84
|
+
return !(perFile && Object.keys(perFile).length > 0);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
59
88
|
export function parseSecurityBaseline(raw: unknown): SecurityBaseline | undefined {
|
|
60
89
|
if (typeof raw !== "object" || raw === null) return undefined;
|
|
61
|
-
if (!(
|
|
62
|
-
if (!("repo" in raw) || typeof raw.repo !== "string") return undefined;
|
|
63
|
-
if (!("generated" in raw) || typeof raw.generated !== "string") return undefined;
|
|
64
|
-
if (!("total" in raw) || typeof raw.total !== "number") return undefined;
|
|
65
|
-
if (!("findings" in raw) || !isFindingsShape(raw.findings)) return undefined;
|
|
90
|
+
if (!hasBaselineCore(raw)) return undefined;
|
|
66
91
|
|
|
67
92
|
let hardFail: readonly string[] | undefined;
|
|
68
93
|
if ("hardFail" in raw && raw.hardFail !== undefined) {
|
|
69
|
-
if (!
|
|
70
|
-
for (const guardName of raw.hardFail) {
|
|
71
|
-
const perFile = raw.findings[guardName];
|
|
72
|
-
// {} is allowed (guard has no current findings); any entry means it isn't done migrating yet.
|
|
73
|
-
if (perFile && Object.keys(perFile).length > 0) return undefined;
|
|
74
|
-
}
|
|
94
|
+
if (!isValidHardFail(raw.hardFail, raw.findings)) return undefined;
|
|
75
95
|
hardFail = raw.hardFail;
|
|
76
96
|
}
|
|
77
97
|
|
|
@@ -107,7 +127,7 @@ export function loadSecurityBaseline(repo: string, repoDir: string): SecurityBas
|
|
|
107
127
|
}
|
|
108
128
|
const parsed = parseSecurityBaseline(raw);
|
|
109
129
|
if (!parsed) {
|
|
110
|
-
return { kind: "invalid", file, reason: "
|
|
130
|
+
return { kind: "invalid", file, reason: "unexpected_baseline_shape" };
|
|
111
131
|
}
|
|
112
132
|
if (parsed.repo !== repo) {
|
|
113
133
|
return {
|
|
@@ -133,36 +153,36 @@ export function locateFinding(
|
|
|
133
153
|
return { repo: root.name, relPath: relative(root.absPath, abs).split(sep).join("/") };
|
|
134
154
|
}
|
|
135
155
|
|
|
136
|
-
|
|
137
|
-
readonly guardName: string;
|
|
138
|
-
readonly violations: readonly GuardViolation[];
|
|
139
|
-
readonly roots: readonly { readonly name: string; readonly absPath: string }[];
|
|
140
|
-
readonly cwd: string;
|
|
141
|
-
readonly load: (repo: string) => SecurityBaselineLoad;
|
|
142
|
-
/** Also fail on baseline headroom (current < baseline) across every root, not just repos with current findings. */
|
|
143
|
-
readonly strict?: boolean;
|
|
144
|
-
}): { readonly blocking: GuardViolation[]; readonly frozen: number; readonly reduced: number } {
|
|
145
|
-
const { guardName, violations, roots, cwd, load, strict } = args;
|
|
156
|
+
type RepoRootList = readonly { readonly name: string; readonly absPath: string }[];
|
|
146
157
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
type LocatedViolation = {
|
|
159
|
+
readonly relPath: string;
|
|
160
|
+
readonly index: number;
|
|
161
|
+
readonly violation: GuardViolation;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
type FindingsByRepo = Map<string, Map<string, LocatedViolation[]>>;
|
|
165
|
+
|
|
166
|
+
// (sortKey, violation) so the final blocking list preserves the original
|
|
167
|
+
// violation order even after synthetic baseline-file entries are spliced in.
|
|
168
|
+
type BlockingEntry = [number, GuardViolation];
|
|
156
169
|
|
|
157
|
-
|
|
170
|
+
function groupFindingsByRepo(
|
|
171
|
+
violations: readonly GuardViolation[],
|
|
172
|
+
roots: RepoRootList,
|
|
173
|
+
cwd: string,
|
|
174
|
+
blockingEntries: BlockingEntry[],
|
|
175
|
+
): FindingsByRepo {
|
|
176
|
+
const byRepo: FindingsByRepo = new Map();
|
|
177
|
+
for (const [index, violation] of violations.entries()) {
|
|
158
178
|
if (violation.neverFrozen) {
|
|
159
179
|
blockingEntries.push([index, violation]);
|
|
160
|
-
|
|
180
|
+
continue;
|
|
161
181
|
}
|
|
162
182
|
const located = locateFinding(violation.file, roots, cwd);
|
|
163
183
|
if (!located) {
|
|
164
184
|
blockingEntries.push([index, violation]);
|
|
165
|
-
|
|
185
|
+
continue;
|
|
166
186
|
}
|
|
167
187
|
let perRepo = byRepo.get(located.repo);
|
|
168
188
|
if (!perRepo) {
|
|
@@ -172,110 +192,167 @@ export function applySecurityBaseline(args: {
|
|
|
172
192
|
const bucket = perRepo.get(located.relPath) ?? [];
|
|
173
193
|
bucket.push({ relPath: located.relPath, index, violation });
|
|
174
194
|
perRepo.set(located.relPath, bucket);
|
|
175
|
-
}
|
|
195
|
+
}
|
|
196
|
+
return byRepo;
|
|
197
|
+
}
|
|
176
198
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
199
|
+
function invalidBaselineViolation(
|
|
200
|
+
baseline: Extract<SecurityBaselineLoad, { kind: "invalid" }>,
|
|
201
|
+
): GuardViolation {
|
|
202
|
+
return {
|
|
203
|
+
file: baseline.file,
|
|
204
|
+
line: 1,
|
|
205
|
+
message: `Security baseline unreadable or invalid: ${baseline.reason}. Fix the file; a broken baseline must not release any findings.`,
|
|
184
206
|
};
|
|
207
|
+
}
|
|
185
208
|
|
|
186
|
-
|
|
187
|
-
|
|
209
|
+
function countsPerPath(
|
|
210
|
+
byPath: ReadonlyMap<string, readonly LocatedViolation[]>,
|
|
211
|
+
): Record<string, number> {
|
|
212
|
+
const current: Record<string, number> = {};
|
|
213
|
+
for (const [relPath, entries] of byPath) current[relPath] = entries.length;
|
|
214
|
+
return current;
|
|
215
|
+
}
|
|
188
216
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
217
|
+
function applyRepoBaseline(args: {
|
|
218
|
+
readonly guardName: string;
|
|
219
|
+
readonly repo: string;
|
|
220
|
+
readonly byPath: ReadonlyMap<string, readonly LocatedViolation[]>;
|
|
221
|
+
readonly baseline: SecurityBaselineLoad;
|
|
222
|
+
readonly blockingEntries: BlockingEntry[];
|
|
223
|
+
}): { readonly frozen: number; readonly reduced: number } {
|
|
224
|
+
const { guardName, repo, byPath, baseline, blockingEntries } = args;
|
|
225
|
+
const allEntries = [...byPath.values()].flat();
|
|
226
|
+
if (baseline.kind === "invalid") {
|
|
227
|
+
const minIndex = Math.min(...allEntries.map((e) => e.index));
|
|
228
|
+
blockingEntries.push([minIndex - 0.5, invalidBaselineViolation(baseline)]);
|
|
229
|
+
for (const e of allEntries) blockingEntries.push([e.index, e.violation]);
|
|
230
|
+
return { frozen: 0, reduced: 0 };
|
|
231
|
+
}
|
|
232
|
+
if (baseline.hardFail.includes(guardName)) {
|
|
233
|
+
for (const e of allEntries) {
|
|
194
234
|
blockingEntries.push([
|
|
195
|
-
|
|
235
|
+
e.index,
|
|
196
236
|
{
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
message: `Security baseline unreadable or invalid: ${baseline.reason}. Fix the file; a broken baseline must not release any findings.`,
|
|
237
|
+
...e.violation,
|
|
238
|
+
message: `${e.violation.message} (security baseline ${repo}: ${guardName} finished migrating — no baseline tolerance)`,
|
|
200
239
|
},
|
|
201
240
|
]);
|
|
202
|
-
for (const e of allEntries) blockingEntries.push([e.index, e.violation]);
|
|
203
|
-
continue;
|
|
204
241
|
}
|
|
205
|
-
|
|
206
|
-
|
|
242
|
+
return { frozen: 0, reduced: 0 };
|
|
243
|
+
}
|
|
244
|
+
const baselineForGuard = baseline.findings[guardName] ?? {};
|
|
245
|
+
const { regressions, reduced } = compareToBaseline(countsPerPath(byPath), baselineForGuard);
|
|
246
|
+
const regressionByPath = new Map(regressions.map((r) => [r.file, r]));
|
|
247
|
+
let frozen = 0;
|
|
248
|
+
for (const [relPath, entries] of byPath) {
|
|
249
|
+
const regression = regressionByPath.get(relPath);
|
|
250
|
+
if (regression) {
|
|
251
|
+
for (const e of entries) {
|
|
207
252
|
blockingEntries.push([
|
|
208
253
|
e.index,
|
|
209
254
|
{
|
|
210
255
|
...e.violation,
|
|
211
|
-
message: `${e.violation.message} (security baseline ${repo}:
|
|
256
|
+
message: `${e.violation.message} (security baseline ${repo}: allowed=${regression.baseline}, current=${regression.current})`,
|
|
212
257
|
},
|
|
213
258
|
]);
|
|
214
259
|
}
|
|
260
|
+
} else {
|
|
261
|
+
frozen += entries.length;
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return { frozen, reduced };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Strict mode also visits repos without current findings, so baseline headroom
|
|
268
|
+
// there fails too. Returns the headroom that counts as reduced.
|
|
269
|
+
function applyStrictHeadroom(args: {
|
|
270
|
+
readonly guardName: string;
|
|
271
|
+
readonly roots: RepoRootList;
|
|
272
|
+
readonly byRepo: FindingsByRepo;
|
|
273
|
+
readonly loadOnce: (repo: string) => SecurityBaselineLoad;
|
|
274
|
+
readonly firstSortKey: number;
|
|
275
|
+
readonly blockingEntries: BlockingEntry[];
|
|
276
|
+
}): number {
|
|
277
|
+
const { guardName, roots, byRepo, loadOnce, firstSortKey, blockingEntries } = args;
|
|
278
|
+
let strictIndex = 0;
|
|
279
|
+
let reduced = 0;
|
|
280
|
+
for (const root of roots) {
|
|
281
|
+
const baseline = loadOnce(root.name);
|
|
282
|
+
const byPath = byRepo.get(root.name);
|
|
283
|
+
if (baseline.kind === "invalid") {
|
|
284
|
+
// Repos with current findings already got this entry in the per-repo pass.
|
|
285
|
+
if (byPath) continue;
|
|
286
|
+
blockingEntries.push([firstSortKey + strictIndex++, invalidBaselineViolation(baseline)]);
|
|
215
287
|
continue;
|
|
216
288
|
}
|
|
217
|
-
const current
|
|
218
|
-
for (const [relPath, entries] of byPath) current[relPath] = entries.length;
|
|
289
|
+
const current = byPath ? countsPerPath(byPath) : {};
|
|
219
290
|
const baselineForGuard = baseline.findings[guardName] ?? {};
|
|
220
|
-
const {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
for (const [relPath, entries] of byPath) {
|
|
224
|
-
const regression = regressionByPath.get(relPath);
|
|
225
|
-
if (regression) {
|
|
226
|
-
for (const e of entries) {
|
|
227
|
-
blockingEntries.push([
|
|
228
|
-
e.index,
|
|
229
|
-
{
|
|
230
|
-
...e.violation,
|
|
231
|
-
message: `${e.violation.message} (security baseline ${repo}: allowed=${regression.baseline}, current=${regression.current})`,
|
|
232
|
-
},
|
|
233
|
-
]);
|
|
234
|
-
}
|
|
235
|
-
} else {
|
|
236
|
-
frozen += entries.length;
|
|
237
|
-
}
|
|
291
|
+
const { reductions } = compareToBaseline(current, baselineForGuard);
|
|
292
|
+
if (!byPath) {
|
|
293
|
+
reduced += reductions.reduce((sum, r) => sum + (r.baseline - r.current), 0);
|
|
238
294
|
}
|
|
295
|
+
for (const r of reductions) {
|
|
296
|
+
blockingEntries.push([
|
|
297
|
+
firstSortKey + strictIndex++,
|
|
298
|
+
{
|
|
299
|
+
file: join(root.absPath, r.file),
|
|
300
|
+
line: 1,
|
|
301
|
+
message: `Security baseline stale: ${root.name}/${r.file} allows ${r.baseline}, found ${r.current} — run \`--write-security-baseline\` and commit, otherwise the headroom covers new findings.`,
|
|
302
|
+
},
|
|
303
|
+
]);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
return reduced;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
export function applySecurityBaseline(args: {
|
|
310
|
+
readonly guardName: string;
|
|
311
|
+
readonly violations: readonly GuardViolation[];
|
|
312
|
+
readonly roots: RepoRootList;
|
|
313
|
+
readonly cwd: string;
|
|
314
|
+
readonly load: (repo: string) => SecurityBaselineLoad;
|
|
315
|
+
/** Also fail on baseline headroom (current < baseline) across every root, not just repos with current findings. */
|
|
316
|
+
readonly strict?: boolean;
|
|
317
|
+
}): { readonly blocking: GuardViolation[]; readonly frozen: number; readonly reduced: number } {
|
|
318
|
+
const { guardName, violations, roots, cwd, load, strict } = args;
|
|
319
|
+
|
|
320
|
+
const blockingEntries: BlockingEntry[] = [];
|
|
321
|
+
const byRepo = groupFindingsByRepo(violations, roots, cwd, blockingEntries);
|
|
322
|
+
|
|
323
|
+
const loadCache = new Map<string, SecurityBaselineLoad>();
|
|
324
|
+
const loadOnce = (repo: string): SecurityBaselineLoad => {
|
|
325
|
+
const cached = loadCache.get(repo);
|
|
326
|
+
if (cached) return cached;
|
|
327
|
+
const result = load(repo);
|
|
328
|
+
loadCache.set(repo, result);
|
|
329
|
+
return result;
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
let frozen = 0;
|
|
333
|
+
let reduced = 0;
|
|
334
|
+
|
|
335
|
+
for (const [repo, byPath] of byRepo) {
|
|
336
|
+
const repoResult = applyRepoBaseline({
|
|
337
|
+
guardName,
|
|
338
|
+
repo,
|
|
339
|
+
byPath,
|
|
340
|
+
baseline: loadOnce(repo),
|
|
341
|
+
blockingEntries,
|
|
342
|
+
});
|
|
343
|
+
frozen += repoResult.frozen;
|
|
344
|
+
reduced += repoResult.reduced;
|
|
239
345
|
}
|
|
240
346
|
|
|
241
347
|
if (strict === true) {
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
violations.length + strictIndex++,
|
|
251
|
-
{
|
|
252
|
-
file: baseline.file,
|
|
253
|
-
line: 1,
|
|
254
|
-
message: `Security baseline unreadable or invalid: ${baseline.reason}. Fix the file; a broken baseline must not release any findings.`,
|
|
255
|
-
},
|
|
256
|
-
]);
|
|
257
|
-
continue;
|
|
258
|
-
}
|
|
259
|
-
const current: Record<string, number> = {};
|
|
260
|
-
if (byPath) {
|
|
261
|
-
for (const [relPath, entries] of byPath) current[relPath] = entries.length;
|
|
262
|
-
}
|
|
263
|
-
const baselineForGuard = baseline.findings[guardName] ?? {};
|
|
264
|
-
const { reductions } = compareToBaseline(current, baselineForGuard);
|
|
265
|
-
if (!byPath) {
|
|
266
|
-
reduced += reductions.reduce((sum, r) => sum + (r.baseline - r.current), 0);
|
|
267
|
-
}
|
|
268
|
-
for (const r of reductions) {
|
|
269
|
-
blockingEntries.push([
|
|
270
|
-
violations.length + strictIndex++,
|
|
271
|
-
{
|
|
272
|
-
file: join(root.absPath, r.file),
|
|
273
|
-
line: 1,
|
|
274
|
-
message: `Security baseline stale: ${root.name}/${r.file} allows ${r.baseline}, found ${r.current} — run \`--write-security-baseline\` and commit, otherwise the headroom covers new findings.`,
|
|
275
|
-
},
|
|
276
|
-
]);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
348
|
+
reduced += applyStrictHeadroom({
|
|
349
|
+
guardName,
|
|
350
|
+
roots,
|
|
351
|
+
byRepo,
|
|
352
|
+
loadOnce,
|
|
353
|
+
firstSortKey: violations.length,
|
|
354
|
+
blockingEntries,
|
|
355
|
+
});
|
|
279
356
|
}
|
|
280
357
|
|
|
281
358
|
blockingEntries.sort((a, b) => a[0] - b[0]);
|
|
@@ -314,7 +391,7 @@ export function buildSecurityBaseline(
|
|
|
314
391
|
return {
|
|
315
392
|
format: SECURITY_BASELINE_FORMAT,
|
|
316
393
|
repo,
|
|
317
|
-
generated:
|
|
394
|
+
generated: Temporal.Now.instant().toString().slice(0, 10),
|
|
318
395
|
total,
|
|
319
396
|
...(sortedHardFail.length > 0 ? { hardFail: sortedHardFail } : {}),
|
|
320
397
|
findings: Object.fromEntries(Object.entries(findings).sort(([a], [b]) => a.localeCompare(b))),
|
|
@@ -174,6 +174,35 @@ function stripMarker(trimmed: string): string {
|
|
|
174
174
|
return trimmed.slice(0, idx).trimEnd();
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
type LineMarker = { readonly reason: string; readonly line: number };
|
|
178
|
+
|
|
179
|
+
// Documented hatch: marker on the call line or the line directly above.
|
|
180
|
+
// Each marker line suppresses at most one subsequent hit (consumed), so a
|
|
181
|
+
// second `.unsafe()` under the same comment is not silently covered.
|
|
182
|
+
function markerForLine(
|
|
183
|
+
lines: readonly string[],
|
|
184
|
+
i: number,
|
|
185
|
+
consumedMarkers: Set<number>,
|
|
186
|
+
): LineMarker | undefined {
|
|
187
|
+
const sameLineReason = MARKER_WITH_REASON_RE.exec(lines[i] ?? "")?.[2];
|
|
188
|
+
if (sameLineReason !== undefined) return { reason: sameLineReason.trim(), line: i + 1 };
|
|
189
|
+
if (i === 0) return undefined;
|
|
190
|
+
const aboveReason = MARKER_WITH_REASON_RE.exec(lines[i - 1] ?? "")?.[2];
|
|
191
|
+
const markerIdx = i - 1;
|
|
192
|
+
if (aboveReason === undefined || consumedMarkers.has(markerIdx)) return undefined;
|
|
193
|
+
consumedMarkers.add(markerIdx);
|
|
194
|
+
return { reason: aboveReason.trim(), line: i };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function isCommentOnlyLine(trimmed: string): boolean {
|
|
198
|
+
return (
|
|
199
|
+
trimmed.startsWith("//") ||
|
|
200
|
+
trimmed.startsWith("*") ||
|
|
201
|
+
trimmed.startsWith("/**") ||
|
|
202
|
+
trimmed.startsWith("/*")
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
177
206
|
function scanFileText(relPath: string, text: string, hits: SqlInventoryHit[]): void {
|
|
178
207
|
const lines = text.split("\n");
|
|
179
208
|
const consumedMarkers = new Set<number>();
|
|
@@ -185,34 +214,9 @@ function scanFileText(relPath: string, text: string, hits: SqlInventoryHit[]): v
|
|
|
185
214
|
// a marker placed on its own comment line (the common case: the line
|
|
186
215
|
// ABOVE the offending call) must still be seen even though that line
|
|
187
216
|
// itself never reaches the pattern loop.
|
|
188
|
-
|
|
189
|
-
// Documented hatch: marker on the call line or the line directly above.
|
|
190
|
-
// Each marker line suppresses at most one subsequent hit (consumed), so a
|
|
191
|
-
// second `.unsafe()` under the same comment is not silently covered.
|
|
192
|
-
let markerReason: string | undefined;
|
|
193
|
-
let markerLine: number | undefined;
|
|
194
|
-
const sameLineReason = MARKER_WITH_REASON_RE.exec(line)?.[2];
|
|
195
|
-
if (sameLineReason !== undefined) {
|
|
196
|
-
markerReason = sameLineReason.trim();
|
|
197
|
-
markerLine = i + 1;
|
|
198
|
-
} else if (i > 0) {
|
|
199
|
-
const aboveReason = MARKER_WITH_REASON_RE.exec(lines[i - 1] ?? "")?.[2];
|
|
200
|
-
const markerIdx = i - 1;
|
|
201
|
-
if (aboveReason !== undefined && !consumedMarkers.has(markerIdx)) {
|
|
202
|
-
markerReason = aboveReason.trim();
|
|
203
|
-
markerLine = i;
|
|
204
|
-
consumedMarkers.add(markerIdx);
|
|
205
|
-
}
|
|
206
|
-
}
|
|
217
|
+
const marker = markerForLine(lines, i, consumedMarkers);
|
|
207
218
|
|
|
208
|
-
if (
|
|
209
|
-
trimmed.startsWith("//") ||
|
|
210
|
-
trimmed.startsWith("*") ||
|
|
211
|
-
trimmed.startsWith("/**") ||
|
|
212
|
-
trimmed.startsWith("/*")
|
|
213
|
-
) {
|
|
214
|
-
continue;
|
|
215
|
-
}
|
|
219
|
+
if (isCommentOnlyLine(trimmed)) continue;
|
|
216
220
|
for (const { kind, re } of PATTERNS) {
|
|
217
221
|
if (!re.test(line)) continue;
|
|
218
222
|
hits.push({
|
|
@@ -220,10 +224,8 @@ function scanFileText(relPath: string, text: string, hits: SqlInventoryHit[]): v
|
|
|
220
224
|
line: i + 1,
|
|
221
225
|
kind,
|
|
222
226
|
allowed: isRawSqlAllowed(relPath),
|
|
223
|
-
markerSuppressed:
|
|
224
|
-
...(
|
|
225
|
-
? { markerReason, markerLine }
|
|
226
|
-
: {}),
|
|
227
|
+
markerSuppressed: marker !== undefined,
|
|
228
|
+
...(marker !== undefined ? { markerReason: marker.reason, markerLine: marker.line } : {}),
|
|
227
229
|
snippet: stripMarker(trimmed).slice(0, 120),
|
|
228
230
|
});
|
|
229
231
|
}
|
|
@@ -259,7 +261,7 @@ export async function scanRepo(
|
|
|
259
261
|
}
|
|
260
262
|
|
|
261
263
|
return {
|
|
262
|
-
scannedAt:
|
|
264
|
+
scannedAt: Temporal.Now.instant().toString(),
|
|
263
265
|
root: repoRoot,
|
|
264
266
|
hits,
|
|
265
267
|
scannedFiles: relFiles.length,
|
package/src/changes.json
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "0.357.0",
|
|
4
|
+
"type": "fix",
|
|
5
|
+
"title": "comment-lang reports an unreadable or non-object baseline instead of crashing",
|
|
6
|
+
"detail": "`kumiko-guards comment-lang` with a baseline file that is not valid JSON, is `null` or has a `null` perFile now prints a baseline-format message and exits 1 instead of throwing a TypeError. A security baseline with the wrong shape now reports the reason `unexpected_baseline_shape`."
|
|
7
|
+
},
|
|
2
8
|
{
|
|
3
9
|
"version": "0.355.0",
|
|
4
10
|
"type": "breaking",
|