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/analyze-Cccs9TDD.js +5454 -0
- package/dist/analyze-Cccs9TDD.js.map +1 -0
- package/dist/cli.d.ts +2 -1
- package/dist/cli.js +433 -407
- package/dist/cli.js.map +1 -1
- package/dist/engine-C4KGqeYw.js +479 -0
- package/dist/engine-C4KGqeYw.js.map +1 -0
- package/dist/index.d.ts +236 -226
- package/dist/index.js +3 -15
- package/dist/worker.js +46 -44
- package/dist/worker.js.map +1 -1
- package/package.json +8 -7
- package/dist/chunk-3Y76J2K2.js +0 -511
- package/dist/chunk-3Y76J2K2.js.map +0 -1
- package/dist/chunk-Z34GRD3S.js +0 -5352
- package/dist/chunk-Z34GRD3S.js.map +0 -1
- package/dist/index.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,15 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
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
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
package/dist/worker.js.map
CHANGED
|
@@ -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":"
|
|
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.
|
|
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": "
|
|
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": ">=
|
|
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": "^
|
|
61
|
+
"typescript": "^6.0.3"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
64
|
"@biomejs/biome": "^1.9.4",
|
|
65
|
-
"@types/node": "^
|
|
66
|
-
"
|
|
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
|
}
|
package/dist/chunk-3Y76J2K2.js
DELETED
|
@@ -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
|