unguard 0.16.0 → 0.16.2

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/index.js CHANGED
@@ -1,15 +1,3 @@
1
- import {
2
- executeScan,
3
- scan
4
- } from "./chunk-3Y76J2K2.js";
5
- import {
6
- allRules,
7
- getRuleMetadata
8
- } from "./chunk-Z34GRD3S.js";
9
- export {
10
- allRules,
11
- executeScan,
12
- getRuleMetadata,
13
- scan
14
- };
15
- //# sourceMappingURL=index.js.map
1
+ import { i as getRuleMetadata, r as allRules } from "./analyze-Cccs9TDD.js";
2
+ import { n as scan, t as executeScan } from "./engine-C4KGqeYw.js";
3
+ export { allRules, executeScan, getRuleMetadata, scan };
package/dist/worker.js CHANGED
@@ -1,49 +1,51 @@
1
- import {
2
- allRules,
3
- analyzeGroup,
4
- isTSRule
5
- } from "./chunk-Z34GRD3S.js";
6
-
7
- // src/scan/worker.ts
8
- import { parentPort } from "worker_threads";
9
- if (parentPort === null) {
10
- throw new Error("unguard worker entry must run in a worker_threads context");
11
- }
12
- var port = parentPort;
13
- var ruleById = new Map(allRules.map((rule) => [rule.id, rule]));
1
+ import { a as isTSRule, n as analyzeGroup, r as allRules } from "./analyze-Cccs9TDD.js";
2
+ import { parentPort } from "node:worker_threads";
3
+ //#region src/scan/worker.ts
4
+ if (parentPort === null) throw new Error("unguard worker entry must run in a worker_threads context");
5
+ const port = parentPort;
6
+ const ruleById = new Map(allRules.map((rule) => [rule.id, rule]));
14
7
  port.on("message", (req) => {
15
- try {
16
- const rules = resolveRules(req.ruleSpecs);
17
- const tsRules = rules.filter(isTSRule);
18
- const crossFileRules = rules.filter((r) => !isTSRule(r));
19
- const indexNeeds = new Set(req.indexNeeds);
20
- const result = analyzeGroup(req.groupConfig, tsRules, crossFileRules, indexNeeds);
21
- const response = {
22
- taskId: req.taskId,
23
- ok: true,
24
- diagnostics: result.diagnostics,
25
- globalFacts: result.globalFacts
26
- };
27
- port.postMessage(response);
28
- } catch (err) {
29
- const message = err instanceof Error ? `${err.message}
30
- ${err.stack ?? ""}` : String(err);
31
- const response = { taskId: req.taskId, ok: false, error: message };
32
- port.postMessage(response);
33
- }
8
+ try {
9
+ const rules = resolveRules(req.ruleSpecs);
10
+ const tsRules = rules.filter(isTSRule);
11
+ const crossFileRules = rules.filter((r) => !isTSRule(r));
12
+ const indexNeeds = new Set(req.indexNeeds);
13
+ const result = analyzeGroup(req.groupConfig, tsRules, crossFileRules, indexNeeds);
14
+ const response = {
15
+ taskId: req.taskId,
16
+ ok: true,
17
+ diagnostics: result.diagnostics,
18
+ globalFacts: result.globalFacts
19
+ };
20
+ port.postMessage(response);
21
+ } catch (err) {
22
+ const message = err instanceof Error ? `${err.message}\n${err.stack ?? ""}` : String(err);
23
+ const response = {
24
+ taskId: req.taskId,
25
+ ok: false,
26
+ error: message
27
+ };
28
+ port.postMessage(response);
29
+ }
34
30
  });
35
31
  function resolveRules(specs) {
36
- const rules = [];
37
- for (const spec of specs) {
38
- const rule = ruleById.get(spec.id);
39
- if (rule === void 0) continue;
40
- if (spec.severity === "off") continue;
41
- if (rule.severity === spec.severity) {
42
- rules.push(rule);
43
- continue;
44
- }
45
- rules.push({ ...rule, severity: spec.severity });
46
- }
47
- return rules;
32
+ const rules = [];
33
+ for (const spec of specs) {
34
+ const rule = ruleById.get(spec.id);
35
+ if (rule === void 0) continue;
36
+ if (spec.severity === "off") continue;
37
+ if (rule.severity === spec.severity) {
38
+ rules.push(rule);
39
+ continue;
40
+ }
41
+ rules.push({
42
+ ...rule,
43
+ severity: spec.severity
44
+ });
45
+ }
46
+ return rules;
48
47
  }
48
+ //#endregion
49
+ export {};
50
+
49
51
  //# sourceMappingURL=worker.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/scan/worker.ts"],"sourcesContent":["import { parentPort } from \"node:worker_threads\";\nimport { allRules } from \"../rules/index.ts\";\nimport { isTSRule, type CrossFileRule, type ProjectIndexNeed, type Rule } from \"../rules/types.ts\";\nimport { analyzeGroup } from \"./analyze.ts\";\nimport type { WorkerRequest, WorkerResponse, RuleSpec } from \"./worker-pool.ts\";\n\nif (parentPort === null) {\n throw new Error(\"unguard worker entry must run in a worker_threads context\");\n}\n\nconst port = parentPort;\nconst ruleById = new Map<string, Rule>(allRules.map((rule) => [rule.id, rule]));\n\nport.on(\"message\", (req: WorkerRequest) => {\n try {\n const rules = resolveRules(req.ruleSpecs);\n const tsRules = rules.filter(isTSRule);\n const crossFileRules = rules.filter((r): r is CrossFileRule => !isTSRule(r));\n const indexNeeds: ReadonlySet<ProjectIndexNeed> = new Set(req.indexNeeds);\n const result = analyzeGroup(req.groupConfig, tsRules, crossFileRules, indexNeeds);\n const response: WorkerResponse = {\n taskId: req.taskId,\n ok: true,\n diagnostics: result.diagnostics,\n globalFacts: result.globalFacts,\n };\n port.postMessage(response);\n } catch (err) {\n // @unguard no-swallowed-catch worker boundary: forwarded as a structured response to the parent thread.\n const message = err instanceof Error ? `${err.message}\\n${err.stack ?? \"\"}` : String(err);\n const response: WorkerResponse = { taskId: req.taskId, ok: false, error: message };\n port.postMessage(response);\n }\n});\n\nfunction resolveRules(specs: RuleSpec[]): Rule[] {\n const rules: Rule[] = [];\n for (const spec of specs) {\n const rule = ruleById.get(spec.id);\n if (rule === undefined) continue;\n if (spec.severity === \"off\") continue;\n if (rule.severity === spec.severity) {\n rules.push(rule);\n continue;\n }\n rules.push({ ...rule, severity: spec.severity });\n }\n return rules;\n}\n"],"mappings":";;;;;;;AAAA,SAAS,kBAAkB;AAM3B,IAAI,eAAe,MAAM;AACvB,QAAM,IAAI,MAAM,2DAA2D;AAC7E;AAEA,IAAM,OAAO;AACb,IAAM,WAAW,IAAI,IAAkB,SAAS,IAAI,CAAC,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AAE9E,KAAK,GAAG,WAAW,CAAC,QAAuB;AACzC,MAAI;AACF,UAAM,QAAQ,aAAa,IAAI,SAAS;AACxC,UAAM,UAAU,MAAM,OAAO,QAAQ;AACrC,UAAM,iBAAiB,MAAM,OAAO,CAAC,MAA0B,CAAC,SAAS,CAAC,CAAC;AAC3E,UAAM,aAA4C,IAAI,IAAI,IAAI,UAAU;AACxE,UAAM,SAAS,aAAa,IAAI,aAAa,SAAS,gBAAgB,UAAU;AAChF,UAAM,WAA2B;AAAA,MAC/B,QAAQ,IAAI;AAAA,MACZ,IAAI;AAAA,MACJ,aAAa,OAAO;AAAA,MACpB,aAAa,OAAO;AAAA,IACtB;AACA,SAAK,YAAY,QAAQ;AAAA,EAC3B,SAAS,KAAK;AAEZ,UAAM,UAAU,eAAe,QAAQ,GAAG,IAAI,OAAO;AAAA,EAAK,IAAI,SAAS,EAAE,KAAK,OAAO,GAAG;AACxF,UAAM,WAA2B,EAAE,QAAQ,IAAI,QAAQ,IAAI,OAAO,OAAO,QAAQ;AACjF,SAAK,YAAY,QAAQ;AAAA,EAC3B;AACF,CAAC;AAED,SAAS,aAAa,OAA2B;AAC/C,QAAM,QAAgB,CAAC;AACvB,aAAW,QAAQ,OAAO;AACxB,UAAM,OAAO,SAAS,IAAI,KAAK,EAAE;AACjC,QAAI,SAAS,OAAW;AACxB,QAAI,KAAK,aAAa,MAAO;AAC7B,QAAI,KAAK,aAAa,KAAK,UAAU;AACnC,YAAM,KAAK,IAAI;AACf;AAAA,IACF;AACA,UAAM,KAAK,EAAE,GAAG,MAAM,UAAU,KAAK,SAAS,CAAC;AAAA,EACjD;AACA,SAAO;AACT;","names":[]}
1
+ {"version":3,"file":"worker.js","names":[],"sources":["../src/scan/worker.ts"],"sourcesContent":["import { parentPort } from \"node:worker_threads\";\nimport { allRules } from \"../rules/index.ts\";\nimport { isTSRule, type CrossFileRule, type ProjectIndexNeed, type Rule } from \"../rules/types.ts\";\nimport { analyzeGroup } from \"./analyze.ts\";\nimport type { WorkerRequest, WorkerResponse, RuleSpec } from \"./worker-pool.ts\";\n\nif (parentPort === null) {\n throw new Error(\"unguard worker entry must run in a worker_threads context\");\n}\n\nconst port = parentPort;\nconst ruleById = new Map<string, Rule>(allRules.map((rule) => [rule.id, rule]));\n\nport.on(\"message\", (req: WorkerRequest) => {\n try {\n const rules = resolveRules(req.ruleSpecs);\n const tsRules = rules.filter(isTSRule);\n const crossFileRules = rules.filter((r): r is CrossFileRule => !isTSRule(r));\n const indexNeeds: ReadonlySet<ProjectIndexNeed> = new Set(req.indexNeeds);\n const result = analyzeGroup(req.groupConfig, tsRules, crossFileRules, indexNeeds);\n const response: WorkerResponse = {\n taskId: req.taskId,\n ok: true,\n diagnostics: result.diagnostics,\n globalFacts: result.globalFacts,\n };\n port.postMessage(response);\n } catch (err) {\n // @unguard no-swallowed-catch worker boundary: forwarded as a structured response to the parent thread.\n const message = err instanceof Error ? `${err.message}\\n${err.stack ?? \"\"}` : String(err);\n const response: WorkerResponse = { taskId: req.taskId, ok: false, error: message };\n port.postMessage(response);\n }\n});\n\nfunction resolveRules(specs: RuleSpec[]): Rule[] {\n const rules: Rule[] = [];\n for (const spec of specs) {\n const rule = ruleById.get(spec.id);\n if (rule === undefined) continue;\n if (spec.severity === \"off\") continue;\n if (rule.severity === spec.severity) {\n rules.push(rule);\n continue;\n }\n rules.push({ ...rule, severity: spec.severity });\n }\n return rules;\n}\n"],"mappings":";;;AAMA,IAAI,eAAe,MACjB,MAAM,IAAI,MAAM,2DAA2D;AAG7E,MAAM,OAAO;AACb,MAAM,WAAW,IAAI,IAAkB,SAAS,KAAK,SAAS,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC;AAE9E,KAAK,GAAG,YAAY,QAAuB;CACzC,IAAI;EACF,MAAM,QAAQ,aAAa,IAAI,SAAS;EACxC,MAAM,UAAU,MAAM,OAAO,QAAQ;EACrC,MAAM,iBAAiB,MAAM,QAAQ,MAA0B,CAAC,SAAS,CAAC,CAAC;EAC3E,MAAM,aAA4C,IAAI,IAAI,IAAI,UAAU;EACxE,MAAM,SAAS,aAAa,IAAI,aAAa,SAAS,gBAAgB,UAAU;EAChF,MAAM,WAA2B;GAC/B,QAAQ,IAAI;GACZ,IAAI;GACJ,aAAa,OAAO;GACpB,aAAa,OAAO;EACtB;EACA,KAAK,YAAY,QAAQ;CAC3B,SAAS,KAAK;EAEZ,MAAM,UAAU,eAAe,QAAQ,GAAG,IAAI,QAAQ,IAAI,IAAI,SAAS,OAAO,OAAO,GAAG;EACxF,MAAM,WAA2B;GAAE,QAAQ,IAAI;GAAQ,IAAI;GAAO,OAAO;EAAQ;EACjF,KAAK,YAAY,QAAQ;CAC3B;AACF,CAAC;AAED,SAAS,aAAa,OAA2B;CAC/C,MAAM,QAAgB,CAAC;CACvB,KAAK,MAAM,QAAQ,OAAO;EACxB,MAAM,OAAO,SAAS,IAAI,KAAK,EAAE;EACjC,IAAI,SAAS,KAAA,GAAW;EACxB,IAAI,KAAK,aAAa,OAAO;EAC7B,IAAI,KAAK,aAAa,KAAK,UAAU;GACnC,MAAM,KAAK,IAAI;GACf;EACF;EACA,MAAM,KAAK;GAAE,GAAG;GAAM,UAAU,KAAK;EAAS,CAAC;CACjD;CACA,OAAO;AACT"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unguard",
3
- "version": "0.16.0",
3
+ "version": "0.16.2",
4
4
  "description": "Detect overdefensive AI-generated code",
5
5
  "author": "Seva Maltsev <me@seva.dev> (https://seva.dev)",
6
6
  "license": "MIT",
@@ -40,8 +40,8 @@
40
40
  "schema.json"
41
41
  ],
42
42
  "scripts": {
43
- "build": "tsup",
44
- "typecheck": "tsc",
43
+ "build": "tsdown",
44
+ "typecheck": "node ./node_modules/tsc7/bin/tsc",
45
45
  "lint:biome": "biome lint src tests/**/*.test.ts tests/harness.ts",
46
46
  "lint": "npm run lint:biome",
47
47
  "test": "vitest run",
@@ -52,18 +52,19 @@
52
52
  "prepublishOnly": "npm run build && npm run test"
53
53
  },
54
54
  "engines": {
55
- "node": ">=18"
55
+ "node": ">=22"
56
56
  },
57
57
  "dependencies": {
58
58
  "fast-glob": "^3.3.3",
59
59
  "ignore": "^7.0.5",
60
60
  "picocolors": "^1.1.1",
61
- "typescript": "^5.9.3"
61
+ "typescript": "^6.0.3"
62
62
  },
63
63
  "devDependencies": {
64
64
  "@biomejs/biome": "^1.9.4",
65
- "@types/node": "^25.3.0",
66
- "tsup": "^8.5.1",
65
+ "@types/node": "^22.20.1",
66
+ "tsc7": "npm:typescript@^7.0.2",
67
+ "tsdown": "^0.22.4",
67
68
  "vitest": "^4.0.18"
68
69
  }
69
70
  }
@@ -1,511 +0,0 @@
1
- import {
2
- allRules,
3
- analyzeFiles,
4
- getRuleMetadata
5
- } from "./chunk-Z34GRD3S.js";
6
-
7
- // src/scan/config.ts
8
- var BUILTIN_IGNORE = [
9
- "**/node_modules/**",
10
- "**/dist/**",
11
- "**/.git/**",
12
- "**/*.d.ts",
13
- "**/*.d.cts",
14
- "**/*.d.mts"
15
- ];
16
- var GENERATED_IGNORE = [
17
- "**/*.gen.*",
18
- "**/*.generated.*"
19
- ];
20
- var DEFAULT_FAIL_ON = "info";
21
- function resolveScanConfig(options) {
22
- const paths = options.paths.length > 0 ? options.paths : ["."];
23
- const ignore3 = [...BUILTIN_IGNORE, ...GENERATED_IGNORE, ...options.ignore ?? []];
24
- return {
25
- paths,
26
- mode: options.mode ?? "scan",
27
- strict: options.strict ?? false,
28
- rules: options.rules ? [...options.rules] : null,
29
- ignore: ignore3,
30
- rulePolicy: toRulePolicyEntries(options.rulePolicy),
31
- overrides: toResolvedOverrides(options.overrides),
32
- showSeverities: normalizeSeveritySet(options.showSeverities),
33
- failOn: options.failOn ?? DEFAULT_FAIL_ON,
34
- useGitIgnore: options.useGitIgnore ?? true,
35
- concurrency: options.concurrency,
36
- cache: options.cache ?? true,
37
- baseline: options.baseline ?? null
38
- };
39
- }
40
- function toResolvedOverrides(overrides) {
41
- if (overrides === void 0) return [];
42
- return overrides.map((entry) => ({
43
- files: [...entry.files],
44
- rulePolicy: toRulePolicyEntries(entry.rules)
45
- }));
46
- }
47
- function toRulePolicyEntries(policy) {
48
- if (policy === void 0) return [];
49
- if (Array.isArray(policy)) return [...policy];
50
- const entries = [];
51
- for (const [selector, severity] of Object.entries(policy)) {
52
- entries.push({ selector, severity });
53
- }
54
- return entries;
55
- }
56
- function normalizeSeveritySet(levels) {
57
- if (levels === void 0 || levels.length === 0) return null;
58
- return new Set(levels);
59
- }
60
- function isRulePolicySeverity(value) {
61
- return value === "off" || value === "info" || value === "warning" || value === "error";
62
- }
63
- function isSeverity(value) {
64
- return value === "info" || value === "warning" || value === "error";
65
- }
66
- function isFailOn(value) {
67
- return value === "none" || isSeverity(value);
68
- }
69
-
70
- // src/scan/baseline.ts
71
- import { existsSync, readFileSync, writeFileSync } from "fs";
72
- import { relative, resolve } from "path";
73
- var BASELINE_FILENAME = "unguard.baseline.json";
74
- function buildBaseline(diagnostics, cwd) {
75
- const rules = {};
76
- for (const diagnostic of diagnostics) {
77
- const file = relative(cwd, diagnostic.file);
78
- rules[file] ??= {};
79
- const forFile = rules[file];
80
- forFile[diagnostic.ruleId] = (forFile[diagnostic.ruleId] ?? 0) + 1;
81
- }
82
- return { version: 1, rules };
83
- }
84
- function writeBaseline(baseline, cwd) {
85
- const path = resolve(cwd, BASELINE_FILENAME);
86
- writeFileSync(path, `${JSON.stringify(baseline, null, 2)}
87
- `, "utf8");
88
- return path;
89
- }
90
- function loadBaseline(cwd) {
91
- const path = resolve(cwd, BASELINE_FILENAME);
92
- if (!existsSync(path)) return null;
93
- const parsed = JSON.parse(readFileSync(path, "utf8"));
94
- if (!isBaselineData(parsed)) {
95
- throw new Error(`Invalid baseline in ${BASELINE_FILENAME}: expected {"version": 1, "rules": {<file>: {<ruleId>: count}}}.`);
96
- }
97
- return parsed;
98
- }
99
- function isBaselineData(value) {
100
- if (typeof value !== "object" || value === null) return false;
101
- if (!("version" in value) || value.version !== 1) return false;
102
- if (!("rules" in value) || typeof value.rules !== "object" || value.rules === null) return false;
103
- return Object.values(value.rules).every(
104
- (forFile) => typeof forFile === "object" && forFile !== null && Object.values(forFile).every((count) => typeof count === "number")
105
- );
106
- }
107
- function applyBaseline(diagnostics, baseline, cwd) {
108
- const groups = /* @__PURE__ */ new Map();
109
- for (const diagnostic of diagnostics) {
110
- const key = `${relative(cwd, diagnostic.file)}\0${diagnostic.ruleId}`;
111
- let list = groups.get(key);
112
- if (list === void 0) {
113
- list = [];
114
- groups.set(key, list);
115
- }
116
- list.push(diagnostic);
117
- }
118
- const kept = [];
119
- for (const [key, group] of groups) {
120
- const [file, ruleId] = key.split("\0");
121
- const allowed = baseline.rules[file]?.[ruleId] ?? 0;
122
- if (group.length <= allowed) continue;
123
- kept.push(...group);
124
- }
125
- return kept;
126
- }
127
-
128
- // src/scan/policy.ts
129
- import { relative as relative2 } from "path";
130
- import ignore from "ignore";
131
- function buildRuleDescriptors(rules) {
132
- return rules.map((rule) => {
133
- const metadata = getRuleMetadata(rule.id);
134
- return {
135
- rule,
136
- category: metadata.category,
137
- tags: metadata.tags,
138
- confidence: metadata.confidence
139
- };
140
- });
141
- }
142
- function resolveActiveRules(descriptors, config) {
143
- const selectedRules = config.rules ? new Set(config.rules) : null;
144
- const wantedConfidence = config.mode === "audit" ? "heuristic" : "proven";
145
- const active = [];
146
- for (const descriptor of descriptors) {
147
- if (selectedRules) {
148
- if (!selectedRules.has(descriptor.rule.id)) continue;
149
- } else if (descriptor.confidence !== wantedConfidence) {
150
- continue;
151
- }
152
- const resolvedSeverity = resolveRuleSeverity(descriptor, config);
153
- if (resolvedSeverity === "off") continue;
154
- if (descriptor.rule.severity === resolvedSeverity) {
155
- active.push(descriptor.rule);
156
- continue;
157
- }
158
- active.push({ ...descriptor.rule, severity: resolvedSeverity });
159
- }
160
- return active;
161
- }
162
- function resolveRuleSeverity(descriptor, config) {
163
- let severity = descriptor.rule.severity;
164
- for (const entry of config.rulePolicy) {
165
- if (!matchesSelector(descriptor, entry.selector)) continue;
166
- severity = entry.severity;
167
- }
168
- if (severity === "off") return "off";
169
- if (config.strict) return "error";
170
- return severity;
171
- }
172
- function matchesSelector(descriptor, selector) {
173
- if (selector.startsWith("category:")) {
174
- return descriptor.category === selector.slice("category:".length);
175
- }
176
- if (selector.startsWith("tag:")) {
177
- return descriptor.tags.includes(selector.slice("tag:".length));
178
- }
179
- if (selector.startsWith("confidence:")) {
180
- return descriptor.confidence === selector.slice("confidence:".length);
181
- }
182
- if (selector === descriptor.rule.id) return true;
183
- if (!selector.includes("*")) return false;
184
- const regex = new RegExp(`^${escapeRegex(selector).replaceAll("\\*", ".*")}$`);
185
- return regex.test(descriptor.rule.id);
186
- }
187
- function escapeRegex(value) {
188
- return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
189
- }
190
- function finalizeScanResult(diagnostics, fileCount, config) {
191
- const afterOverrides = applyOverrides(diagnostics, config.overrides, process.cwd());
192
- const effective = config.baseline ? applyBaseline(afterOverrides, config.baseline, process.cwd()) : afterOverrides;
193
- const visibleDiagnostics = config.showSeverities ? effective.filter((d) => config.showSeverities?.has(d.severity)) : effective;
194
- return {
195
- diagnostics,
196
- visibleDiagnostics,
197
- fileCount,
198
- exitCode: computeExitCode(effective, config.failOn)
199
- };
200
- }
201
- function applyOverrides(diagnostics, overrides, cwd) {
202
- if (overrides.length === 0) return diagnostics;
203
- const matchers = overrides.map((entry) => ({
204
- matcher: ignore().add(entry.files),
205
- rulePolicy: entry.rulePolicy
206
- }));
207
- const result = [];
208
- for (const diagnostic of diagnostics) {
209
- const relPath = relative2(cwd, diagnostic.file).replaceAll("\\", "/");
210
- const metadata = getRuleMetadata(diagnostic.ruleId);
211
- const descriptor = {
212
- rule: { id: diagnostic.ruleId },
213
- category: metadata.category,
214
- tags: metadata.tags,
215
- confidence: metadata.confidence
216
- };
217
- let severity = diagnostic.severity;
218
- for (const { matcher, rulePolicy } of matchers) {
219
- if (relPath.startsWith("..") || !matcher.ignores(relPath)) continue;
220
- for (const entry of rulePolicy) {
221
- if (!matchesSelector(descriptor, entry.selector)) continue;
222
- severity = entry.severity;
223
- }
224
- }
225
- if (severity === "off") continue;
226
- result.push(severity === diagnostic.severity ? diagnostic : { ...diagnostic, severity });
227
- }
228
- return result;
229
- }
230
- function toScanResult(execution) {
231
- return {
232
- diagnostics: execution.diagnostics,
233
- fileCount: execution.fileCount
234
- };
235
- }
236
- function computeExitCode(diagnostics, failOn) {
237
- if (failOn === "none") return 0;
238
- const hasError = diagnostics.some((d) => d.severity === "error");
239
- const hasWarning = diagnostics.some((d) => d.severity === "warning");
240
- const hasInfo = diagnostics.some((d) => d.severity === "info");
241
- if (failOn === "error") return hasError ? 2 : 0;
242
- if (failOn === "warning") {
243
- if (hasError) return 2;
244
- return hasWarning ? 1 : 0;
245
- }
246
- if (hasError) return 2;
247
- if (hasWarning || hasInfo) return 1;
248
- return 0;
249
- }
250
-
251
- // src/engine.ts
252
- import { availableParallelism } from "os";
253
-
254
- // src/scan/cache.ts
255
- import { createHash } from "crypto";
256
- import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync2, statSync, writeFileSync as writeFileSync2 } from "fs";
257
- import { join } from "path";
258
- var CACHE_VERSION = 1;
259
- var CACHE_FILENAME = "scan-cache.json";
260
- function resolveCacheDir(startDir) {
261
- let dir = startDir;
262
- for (; ; ) {
263
- const nm = join(dir, "node_modules");
264
- if (existsSync2(nm)) return join(nm, ".cache", "unguard", `v${CACHE_VERSION}`);
265
- const parent = join(dir, "..");
266
- if (parent === dir) return null;
267
- dir = parent;
268
- }
269
- }
270
- function computeScanKey(input) {
271
- const ruleSig = [...input.rules].map((r) => `${r.id}=${r.severity}`).sort().join(",");
272
- const payload = JSON.stringify({
273
- unguardVersion: input.unguardVersion,
274
- ruleSig,
275
- paths: [...input.paths].sort(),
276
- ignore: [...input.ignore].sort(),
277
- strict: input.strict,
278
- failOn: input.failOn,
279
- showSeverities: input.showSeverities ? [...input.showSeverities].sort() : null
280
- });
281
- return createHash("sha256").update(payload).digest("hex").slice(0, 16);
282
- }
283
- function hashFile(path, prevFingerprint) {
284
- const s = statSync(path, { bigint: true });
285
- const statTag = `${s.size}:${s.mtimeNs.toString()}`;
286
- if (prevFingerprint !== void 0) {
287
- const prevStat = prevFingerprint.split("@", 1)[0];
288
- if (prevStat === statTag) return prevFingerprint;
289
- }
290
- const buf = readFileSync2(path);
291
- const contentHash = createHash("sha1").update(buf).digest("hex").slice(0, 16);
292
- return `${statTag}@${contentHash}`;
293
- }
294
- function hashFiles(paths, prev) {
295
- const result = {};
296
- for (const path of paths) {
297
- result[path] = hashFile(path, prev?.[path]);
298
- }
299
- return result;
300
- }
301
- function readScanCache(dir) {
302
- const path = join(dir, CACHE_FILENAME);
303
- if (!existsSync2(path)) return null;
304
- const text = trySync(() => readFileSync2(path, "utf8"), "read failed");
305
- if (text === null) return null;
306
- const raw = trySync(() => JSON.parse(text), "parse failed");
307
- if (raw === null) return null;
308
- return validateScanCache(raw);
309
- }
310
- function writeScanCache(dir, entry) {
311
- trySync(() => {
312
- mkdirSync(dir, { recursive: true });
313
- writeFileSync2(join(dir, CACHE_FILENAME), JSON.stringify(entry));
314
- return true;
315
- }, "write failed");
316
- }
317
- function cacheCovers(cached, context, currentHashes) {
318
- if (cached.unguardVersion !== context.unguardVersion) return false;
319
- if (cached.scanKey !== context.scanKey) return false;
320
- const cachedFiles = Object.keys(cached.fileHashes);
321
- const currentFiles = Object.keys(currentHashes);
322
- if (cachedFiles.length !== currentFiles.length) return false;
323
- for (const file of currentFiles) {
324
- const prev = cached.fileHashes[file];
325
- const cur = currentHashes[file];
326
- if (prev === void 0 || cur === void 0) return false;
327
- if (contentHashOf(prev) !== contentHashOf(cur)) return false;
328
- }
329
- return true;
330
- }
331
- function contentHashOf(fingerprint) {
332
- const idx = fingerprint.indexOf("@");
333
- return idx >= 0 ? fingerprint.slice(idx + 1) : fingerprint;
334
- }
335
- function validateScanCache(raw) {
336
- if (typeof raw !== "object" || raw === null) return null;
337
- const entry = raw;
338
- if (entry.version !== CACHE_VERSION) return null;
339
- if (typeof entry.scanKey !== "string") return null;
340
- if (typeof entry.unguardVersion !== "string") return null;
341
- if (typeof entry.fileCount !== "number") return null;
342
- const fileHashes = validateFileHashes(entry.fileHashes);
343
- if (fileHashes === null) return null;
344
- const diagnostics = validateDiagnostics(entry.diagnostics);
345
- if (diagnostics === null) return null;
346
- return {
347
- version: entry.version,
348
- unguardVersion: entry.unguardVersion,
349
- scanKey: entry.scanKey,
350
- fileHashes,
351
- diagnostics,
352
- fileCount: entry.fileCount
353
- };
354
- }
355
- function validateFileHashes(raw) {
356
- if (typeof raw !== "object" || raw === null) return null;
357
- const result = {};
358
- for (const [key, value] of Object.entries(raw)) {
359
- if (typeof value !== "string") return null;
360
- result[key] = value;
361
- }
362
- return result;
363
- }
364
- function validateDiagnostics(raw) {
365
- if (!Array.isArray(raw)) return null;
366
- const list = [];
367
- for (const item of raw) {
368
- const diag = validateDiagnostic(item);
369
- if (diag === null) return null;
370
- list.push(diag);
371
- }
372
- return list;
373
- }
374
- function validateDiagnostic(item) {
375
- if (typeof item !== "object" || item === null) return null;
376
- if (!("file" in item) || typeof item.file !== "string") return null;
377
- if (!("ruleId" in item) || typeof item.ruleId !== "string") return null;
378
- if (!("line" in item) || typeof item.line !== "number") return null;
379
- if (!("column" in item) || typeof item.column !== "number") return null;
380
- if (!("severity" in item)) return null;
381
- if (item.severity !== "error" && item.severity !== "warning" && item.severity !== "info") return null;
382
- if (!("message" in item) || typeof item.message !== "string") return null;
383
- const annotation = "annotation" in item ? item.annotation : void 0;
384
- if (annotation !== void 0 && typeof annotation !== "string") return null;
385
- return {
386
- file: item.file,
387
- ruleId: item.ruleId,
388
- line: item.line,
389
- column: item.column,
390
- severity: item.severity,
391
- message: item.message,
392
- annotation
393
- };
394
- }
395
- function trySync(fn, label) {
396
- try {
397
- return fn();
398
- } catch (err) {
399
- return debugLog(label, err);
400
- }
401
- }
402
- function debugLog(label, err) {
403
- if (process.env.UNGUARD_DEBUG !== void 0) {
404
- console.error(`[unguard] cache ${label}:`, err);
405
- }
406
- return null;
407
- }
408
-
409
- // src/scan/discover.ts
410
- import fg from "fast-glob";
411
- import ignore2 from "ignore";
412
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
413
- import { relative as relative3, resolve as resolve2, sep } from "path";
414
- async function discoverFiles(config) {
415
- const globs = expandGlobs(config.paths);
416
- const discoveredFiles = await fg(globs, {
417
- ignore: config.ignore,
418
- absolute: true
419
- });
420
- if (!config.useGitIgnore) return discoveredFiles;
421
- return applyGitIgnore(discoveredFiles);
422
- }
423
- function expandGlobs(paths) {
424
- return paths.map((p) => {
425
- if (p === ".") return "./**/*.{ts,cts,mts,tsx}";
426
- if (p.endsWith("/")) return `${p}**/*.{ts,cts,mts,tsx}`;
427
- if (!p.includes("*") && !p.endsWith(".ts") && !p.endsWith(".tsx") && !p.endsWith(".cts") && !p.endsWith(".mts")) {
428
- return `${p}/**/*.{ts,cts,mts,tsx}`;
429
- }
430
- return p;
431
- });
432
- }
433
- function applyGitIgnore(files) {
434
- const gitIgnorePath = resolve2(process.cwd(), ".gitignore");
435
- if (!existsSync3(gitIgnorePath)) return files;
436
- const matcher = ignore2().add(readFileSync3(gitIgnorePath, "utf8"));
437
- return files.filter((file) => {
438
- const rel = relative3(process.cwd(), file);
439
- if (rel.startsWith("..")) return true;
440
- const normalized = rel.split(sep).join("/");
441
- return !matcher.ignores(normalized);
442
- });
443
- }
444
-
445
- // src/engine.ts
446
- var UNGUARD_VERSION = "0.16.0";
447
- async function executeScan(options) {
448
- const config = resolveScanConfig(options);
449
- const files = await discoverFiles(config);
450
- const descriptors = buildRuleDescriptors(allRules);
451
- const activeRules = resolveActiveRules(descriptors, config);
452
- const cacheDir = config.cache ? resolveCacheDir(process.cwd()) : null;
453
- if (cacheDir !== null) {
454
- const scanKey = computeScanKey({
455
- unguardVersion: UNGUARD_VERSION,
456
- rules: activeRules,
457
- paths: config.paths,
458
- ignore: config.ignore,
459
- strict: config.strict,
460
- failOn: config.failOn,
461
- showSeverities: config.showSeverities
462
- });
463
- const cached = readScanCache(cacheDir);
464
- const sortedFiles = [...files].sort();
465
- const currentHashes = hashFiles(sortedFiles, cached?.fileHashes ?? null);
466
- if (cached !== null && cacheCovers(cached, { unguardVersion: UNGUARD_VERSION, scanKey }, currentHashes)) {
467
- return finalizeScanResult(cached.diagnostics, cached.fileCount, config);
468
- }
469
- const concurrency2 = resolveDefaultConcurrency(config.concurrency);
470
- const diagnostics2 = await analyzeFiles(files, activeRules, { concurrency: concurrency2 });
471
- writeScanCache(cacheDir, {
472
- version: 1,
473
- unguardVersion: UNGUARD_VERSION,
474
- scanKey,
475
- fileHashes: currentHashes,
476
- diagnostics: diagnostics2,
477
- fileCount: files.length
478
- });
479
- return finalizeScanResult(diagnostics2, files.length, config);
480
- }
481
- const concurrency = resolveDefaultConcurrency(config.concurrency);
482
- const diagnostics = await analyzeFiles(files, activeRules, { concurrency });
483
- return finalizeScanResult(diagnostics, files.length, config);
484
- }
485
- function resolveDefaultConcurrency(requested) {
486
- if (requested !== void 0) {
487
- if (!Number.isFinite(requested) || requested < 1) return 1;
488
- return Math.floor(requested);
489
- }
490
- const cores = availableParallelism();
491
- return Math.min(8, Math.max(1, Math.ceil(cores / 2)));
492
- }
493
- async function scan(options) {
494
- const execution = await executeScan(options);
495
- return toScanResult(execution);
496
- }
497
-
498
- export {
499
- toRulePolicyEntries,
500
- isRulePolicySeverity,
501
- isSeverity,
502
- isFailOn,
503
- BASELINE_FILENAME,
504
- buildBaseline,
505
- writeBaseline,
506
- loadBaseline,
507
- computeExitCode,
508
- executeScan,
509
- scan
510
- };
511
- //# sourceMappingURL=chunk-3Y76J2K2.js.map