@cosmicdrift/kumiko-guards 0.403.0 → 0.404.1

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cosmicdrift/kumiko-guards",
3
- "version": "0.403.0",
3
+ "version": "0.404.1",
4
4
  "description": "AST-based security guards for Kumiko repos: direct-fs/fetch, tenant escalation, admin-API, escape hatches and related checks, run over a shared ts-morph project.",
5
5
  "license": "MIT",
6
6
  "author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
@@ -36,7 +36,7 @@
36
36
  "kumiko-pre-push": "./src/pre-push.sh"
37
37
  },
38
38
  "dependencies": {
39
- "@cosmicdrift/kumiko-types": "0.403.0",
39
+ "@cosmicdrift/kumiko-types": "0.404.1",
40
40
  "ts-morph": "^28.0.0",
41
41
  "zod": "^4.6.7"
42
42
  },
@@ -52,7 +52,7 @@
52
52
  "LICENSE"
53
53
  ],
54
54
  "peerDependencies": {
55
- "@cosmicdrift/kumiko-framework": "0.403.0"
55
+ "@cosmicdrift/kumiko-framework": "0.404.1"
56
56
  },
57
57
  "peerDependenciesMeta": {
58
58
  "@cosmicdrift/kumiko-framework": {
@@ -60,6 +60,6 @@
60
60
  }
61
61
  },
62
62
  "devDependencies": {
63
- "@cosmicdrift/kumiko-framework": "0.403.0"
63
+ "@cosmicdrift/kumiko-framework": "0.404.1"
64
64
  }
65
65
  }
@@ -108,7 +108,7 @@ function readExceptionEntries(source: ExceptionSource, filename: string): unknow
108
108
  export function loadExceptionEntries<T>(options: {
109
109
  readonly filename: string;
110
110
  readonly roots: readonly string[];
111
- readonly parse: (field: (key: string) => string) => T;
111
+ readonly parse: (field: (key: string) => string, source: ExceptionSource) => T;
112
112
  readonly bundledDir?: string;
113
113
  }): T[] {
114
114
  const merged: T[] = [];
@@ -121,7 +121,7 @@ export function loadExceptionEntries<T>(options: {
121
121
  const value = entry[key];
122
122
  if (typeof value !== "string" || value.trim() === "") throw new MissingFieldError(key);
123
123
  return value;
124
- }),
124
+ }, source),
125
125
  );
126
126
  } catch (err) {
127
127
  if (!(err instanceof MissingFieldError)) throw err;
@@ -1,7 +1,11 @@
1
1
  /**
2
2
  * Parse and evaluate `bun audit --json` reports with exception support.
3
3
  */
4
- import { isExceptionValid, matchesPackageName } from "./dependency-exceptions";
4
+ import {
5
+ type ExceptionSource,
6
+ isExceptionValid,
7
+ matchesPackageName,
8
+ } from "./dependency-exceptions";
5
9
 
6
10
  export type Severity = "critical" | "high" | "moderate" | "low" | "info";
7
11
 
@@ -29,6 +33,7 @@ export type SecurityException = {
29
33
  readonly reason: string;
30
34
  readonly reviewedBy: string;
31
35
  readonly expires: string;
36
+ readonly source: ExceptionSource;
32
37
  };
33
38
 
34
39
  export type SecurityEvaluation = {
package/src/changes.json CHANGED
@@ -1,4 +1,10 @@
1
1
  [
2
+ {
3
+ "version": "0.404.0",
4
+ "type": "improvement",
5
+ "title": "Security check warns about unused repo exceptions",
6
+ "detail": "kumiko-guards security lists every exception from the repo's dependency-security-exceptions.json that suppressed no advisory in the run, with package, advisory and file. The warning does not block; exceptions bundled with the guards package never warn because they cover consumers that may not have the package. Remove the listed entries once the advisory is fixed or withdrawn."
7
+ },
2
8
  {
3
9
  "version": "0.402.0",
4
10
  "type": "improvement",
@@ -34,12 +34,13 @@ export function loadSecurityExceptions(
34
34
  filename: SECURITY_EXCEPTIONS_FILE,
35
35
  roots,
36
36
  bundledDir,
37
- parse: (field) => ({
37
+ parse: (field, source) => ({
38
38
  advisory: field("advisory"),
39
39
  package: field("package"),
40
40
  reason: field("reason"),
41
41
  reviewedBy: field("reviewedBy"),
42
42
  expires: field("expires"),
43
+ source,
43
44
  }),
44
45
  }).filter((exception) => {
45
46
  const hasDate = expiryEndMs(exception.expires) !== undefined;
@@ -67,6 +68,42 @@ export function shouldBlock(
67
68
  return evaluation.blocking.length > 0 || expiringUrgent.length > 0;
68
69
  }
69
70
 
71
+ // Bundled exceptions apply to every consumer, including those without the package, so only repo-local ones can be stale.
72
+ function findUnusedRepoExceptions(
73
+ exceptions: readonly SecurityException[],
74
+ used: readonly SecurityException[],
75
+ ): SecurityException[] {
76
+ return exceptions.filter((ex) => ex.source.origin === "repo" && !used.includes(ex));
77
+ }
78
+
79
+ function printExceptionWarnings(
80
+ evaluation: SecurityEvaluation,
81
+ exceptions: readonly SecurityException[],
82
+ expiringUrgent: readonly SecurityException[],
83
+ nowMs: number,
84
+ ): void {
85
+ const urgentKeys = new Set(expiringUrgent.map((ex) => `${ex.package}|${ex.advisory}`));
86
+ const expiringSoon = findExpiringSoon(exceptions, 30, nowMs).filter(
87
+ (ex) => !urgentKeys.has(`${ex.package}|${ex.advisory}`),
88
+ );
89
+ if (expiringSoon.length > 0) {
90
+ console.log("\n WARN: security exceptions expire within 30 days — re-review:");
91
+ for (const ex of expiringSoon) {
92
+ console.log(` ${ex.package} ${ex.advisory} expires=${ex.expires}`);
93
+ }
94
+ }
95
+
96
+ const unusedRepoExceptions = findUnusedRepoExceptions(exceptions, evaluation.usedExceptions);
97
+ if (unusedRepoExceptions.length > 0) {
98
+ console.log(
99
+ "\n WARN: security exceptions suppressed no advisory in this run — remove the entry if it is obsolete:",
100
+ );
101
+ for (const ex of unusedRepoExceptions) {
102
+ console.log(` ${ex.package} ${ex.advisory} (${ex.source.file})`);
103
+ }
104
+ }
105
+ }
106
+
70
107
  function printReport(
71
108
  evaluation: SecurityEvaluation,
72
109
  exceptions: readonly SecurityException[],
@@ -112,16 +149,7 @@ function printReport(
112
149
  }
113
150
  }
114
151
 
115
- const urgentKeys = new Set(expiringUrgent.map((ex) => `${ex.package}|${ex.advisory}`));
116
- const expiringSoon = findExpiringSoon(exceptions, 30, nowMs).filter(
117
- (ex) => !urgentKeys.has(`${ex.package}|${ex.advisory}`),
118
- );
119
- if (expiringSoon.length > 0) {
120
- console.log("\n WARN: security exceptions expire within 30 days — re-review:");
121
- for (const ex of expiringSoon) {
122
- console.log(` ${ex.package} ${ex.advisory} expires=${ex.expires}`);
123
- }
124
- }
152
+ printExceptionWarnings(evaluation, exceptions, expiringUrgent, nowMs);
125
153
 
126
154
  if (shouldBlock(evaluation, expiringUrgent)) {
127
155
  console.error(