@aspect-guard/core 0.1.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/dist/index.js ADDED
@@ -0,0 +1,1565 @@
1
+ // src/types/severity.ts
2
+ var SEVERITY_ORDER = {
3
+ critical: 0,
4
+ high: 1,
5
+ medium: 2,
6
+ low: 3,
7
+ info: 4
8
+ };
9
+ function compareSeverity(a, b) {
10
+ return SEVERITY_ORDER[a] - SEVERITY_ORDER[b];
11
+ }
12
+ function isAtLeastSeverity(severity, minimum) {
13
+ return SEVERITY_ORDER[severity] <= SEVERITY_ORDER[minimum];
14
+ }
15
+
16
+ // src/scanner/scanner.ts
17
+ import * as os2 from "os";
18
+ import { randomUUID as randomUUID2 } from "crypto";
19
+
20
+ // src/scanner/ide-detector.ts
21
+ import * as fs from "fs";
22
+ import * as os from "os";
23
+ import * as path from "path";
24
+ var IDE_PATHS = {
25
+ "VS Code": ["~/.vscode/extensions"],
26
+ "VS Code Insiders": ["~/.vscode-insiders/extensions"],
27
+ Cursor: ["~/.cursor/extensions"],
28
+ Windsurf: ["~/.windsurf/extensions"],
29
+ Trae: ["~/.trae/extensions"],
30
+ VSCodium: ["~/.vscode-oss/extensions"]
31
+ };
32
+ function expandPath(inputPath) {
33
+ if (inputPath.startsWith("~")) {
34
+ return path.join(os.homedir(), inputPath.slice(1));
35
+ }
36
+ if (inputPath.includes("%USERPROFILE%")) {
37
+ return inputPath.replace("%USERPROFILE%", os.homedir());
38
+ }
39
+ return inputPath;
40
+ }
41
+ function countExtensions(dirPath) {
42
+ try {
43
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
44
+ return entries.filter((entry) => entry.isDirectory()).length;
45
+ } catch {
46
+ return 0;
47
+ }
48
+ }
49
+ function detectIDEPaths() {
50
+ const detected = [];
51
+ for (const [ideName, paths] of Object.entries(IDE_PATHS)) {
52
+ for (const idePath of paths) {
53
+ const expandedPath = expandPath(idePath);
54
+ if (fs.existsSync(expandedPath)) {
55
+ detected.push({
56
+ name: ideName,
57
+ path: expandedPath,
58
+ extensionCount: countExtensions(expandedPath)
59
+ });
60
+ break;
61
+ }
62
+ }
63
+ }
64
+ return detected;
65
+ }
66
+
67
+ // src/scanner/extension-reader.ts
68
+ import * as fs2 from "fs/promises";
69
+ import * as path2 from "path";
70
+ async function readExtension(extensionPath) {
71
+ try {
72
+ const packageJsonPath = path2.join(extensionPath, "package.json");
73
+ const content = await fs2.readFile(packageJsonPath, "utf-8");
74
+ const manifest = JSON.parse(content);
75
+ if (!manifest.name || !manifest.publisher || !manifest.version) {
76
+ return null;
77
+ }
78
+ const stats = await getDirectoryStats(extensionPath);
79
+ const repository = typeof manifest.repository === "string" ? manifest.repository : manifest.repository?.url;
80
+ return {
81
+ id: `${manifest.publisher}.${manifest.name}`,
82
+ displayName: manifest.displayName ?? manifest.name,
83
+ version: manifest.version,
84
+ publisher: {
85
+ name: manifest.publisher,
86
+ verified: false
87
+ },
88
+ description: manifest.description ?? "",
89
+ categories: manifest.categories ?? [],
90
+ activationEvents: manifest.activationEvents ?? [],
91
+ extensionDependencies: manifest.extensionDependencies ?? [],
92
+ installPath: extensionPath,
93
+ engines: { vscode: manifest.engines?.vscode ?? "*" },
94
+ repository,
95
+ license: manifest.license,
96
+ fileCount: stats.fileCount,
97
+ totalSize: stats.totalSize
98
+ };
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+ async function getDirectoryStats(dirPath) {
104
+ let fileCount = 0;
105
+ let totalSize = 0;
106
+ async function walk(currentPath) {
107
+ try {
108
+ const entries = await fs2.readdir(currentPath, { withFileTypes: true });
109
+ for (const entry of entries) {
110
+ const fullPath = path2.join(currentPath, entry.name);
111
+ if (entry.isDirectory()) {
112
+ if (entry.name !== "node_modules") {
113
+ await walk(fullPath);
114
+ }
115
+ } else {
116
+ fileCount++;
117
+ try {
118
+ const stat3 = await fs2.stat(fullPath);
119
+ totalSize += stat3.size;
120
+ } catch {
121
+ }
122
+ }
123
+ }
124
+ } catch {
125
+ }
126
+ }
127
+ await walk(dirPath);
128
+ return { fileCount, totalSize };
129
+ }
130
+ async function readExtensionsFromDirectory(directoryPath) {
131
+ const extensions = [];
132
+ try {
133
+ const entries = await fs2.readdir(directoryPath, { withFileTypes: true });
134
+ const directories = entries.filter((entry) => entry.isDirectory());
135
+ const results = await Promise.all(
136
+ directories.map(
137
+ (dir) => readExtension(path2.join(directoryPath, dir.name))
138
+ )
139
+ );
140
+ for (const result of results) {
141
+ if (result) {
142
+ extensions.push(result);
143
+ }
144
+ }
145
+ } catch {
146
+ return [];
147
+ }
148
+ return extensions;
149
+ }
150
+
151
+ // src/scanner/file-collector.ts
152
+ import * as fs3 from "fs/promises";
153
+ import * as path3 from "path";
154
+ var COLLECTED_EXTENSIONS = /* @__PURE__ */ new Set([
155
+ ".js",
156
+ ".ts",
157
+ ".jsx",
158
+ ".tsx",
159
+ ".mjs",
160
+ ".cjs",
161
+ ".json"
162
+ ]);
163
+ var IGNORED_DIRECTORIES = /* @__PURE__ */ new Set([
164
+ "node_modules",
165
+ ".git",
166
+ ".svn",
167
+ ".hg",
168
+ "__pycache__"
169
+ ]);
170
+ var IGNORED_PATTERNS = [/\.min\.js$/, /\.map$/, /\.d\.ts$/];
171
+ var MAX_FILE_SIZE = 1024 * 1024;
172
+ function shouldCollectFile(filePath) {
173
+ const ext = path3.extname(filePath).toLowerCase();
174
+ if (!COLLECTED_EXTENSIONS.has(ext)) {
175
+ return false;
176
+ }
177
+ const parts = filePath.split(path3.sep);
178
+ for (const part of parts) {
179
+ if (IGNORED_DIRECTORIES.has(part)) {
180
+ return false;
181
+ }
182
+ }
183
+ for (const pattern of IGNORED_PATTERNS) {
184
+ if (pattern.test(filePath)) {
185
+ return false;
186
+ }
187
+ }
188
+ return true;
189
+ }
190
+ async function collectFiles(extensionPath) {
191
+ const files = /* @__PURE__ */ new Map();
192
+ async function walk(currentPath, relativePath) {
193
+ try {
194
+ const entries = await fs3.readdir(currentPath, { withFileTypes: true });
195
+ for (const entry of entries) {
196
+ const fullPath = path3.join(currentPath, entry.name);
197
+ const relPath = relativePath ? path3.join(relativePath, entry.name) : entry.name;
198
+ if (entry.isDirectory()) {
199
+ if (!IGNORED_DIRECTORIES.has(entry.name)) {
200
+ await walk(fullPath, relPath);
201
+ }
202
+ } else if (entry.isFile()) {
203
+ if (shouldCollectFile(relPath)) {
204
+ try {
205
+ const stat3 = await fs3.stat(fullPath);
206
+ if (stat3.size <= MAX_FILE_SIZE) {
207
+ const content = await fs3.readFile(fullPath, "utf-8");
208
+ files.set(relPath, content);
209
+ }
210
+ } catch {
211
+ }
212
+ }
213
+ }
214
+ }
215
+ } catch {
216
+ }
217
+ }
218
+ await walk(extensionPath, "");
219
+ return files;
220
+ }
221
+
222
+ // src/rules/rule-registry.ts
223
+ var RuleRegistry = class {
224
+ rules = /* @__PURE__ */ new Map();
225
+ register(rule) {
226
+ this.rules.set(rule.id, rule);
227
+ }
228
+ get(id) {
229
+ return this.rules.get(id);
230
+ }
231
+ getAll() {
232
+ return Array.from(this.rules.values());
233
+ }
234
+ getEnabled() {
235
+ return this.getAll().filter((rule) => rule.enabled);
236
+ }
237
+ getByCategory(category) {
238
+ return this.getAll().filter((rule) => rule.category === category);
239
+ }
240
+ getBySeverity(severity) {
241
+ return this.getAll().filter((rule) => rule.severity === severity);
242
+ }
243
+ clear() {
244
+ this.rules.clear();
245
+ }
246
+ };
247
+ var ruleRegistry = new RuleRegistry();
248
+
249
+ // src/rules/rule-engine.ts
250
+ import { randomUUID } from "crypto";
251
+ var RuleEngine = class {
252
+ options;
253
+ constructor(options = {}) {
254
+ this.options = options;
255
+ }
256
+ run(files, manifest) {
257
+ const findings = [];
258
+ const rules = this.getApplicableRules();
259
+ for (const rule of rules) {
260
+ try {
261
+ const evidences = rule.detect(files, manifest);
262
+ for (const evidence of evidences) {
263
+ findings.push(this.createFinding(rule, evidence));
264
+ }
265
+ } catch {
266
+ }
267
+ }
268
+ return findings;
269
+ }
270
+ getApplicableRules() {
271
+ let rules = ruleRegistry.getEnabled();
272
+ if (this.options.rules && this.options.rules.length > 0) {
273
+ rules = rules.filter((r) => this.options.rules.includes(r.id));
274
+ }
275
+ if (this.options.skipRules && this.options.skipRules.length > 0) {
276
+ rules = rules.filter((r) => !this.options.skipRules.includes(r.id));
277
+ }
278
+ if (this.options.minSeverity) {
279
+ const minOrder = SEVERITY_ORDER[this.options.minSeverity];
280
+ rules = rules.filter((r) => SEVERITY_ORDER[r.severity] <= minOrder);
281
+ }
282
+ return rules;
283
+ }
284
+ createFinding(rule, evidence) {
285
+ return {
286
+ id: randomUUID(),
287
+ ruleId: rule.id,
288
+ severity: rule.severity,
289
+ category: rule.category,
290
+ title: rule.name,
291
+ description: rule.description,
292
+ evidence: {
293
+ filePath: evidence.filePath,
294
+ lineNumber: evidence.lineNumber,
295
+ columnNumber: evidence.columnNumber,
296
+ lineContent: evidence.lineContent,
297
+ contextBefore: evidence.contextBefore,
298
+ contextAfter: evidence.contextAfter,
299
+ matchedPattern: evidence.matchedPattern,
300
+ snippet: evidence.snippet
301
+ },
302
+ mitreAttackId: rule.mitreAttackId
303
+ };
304
+ }
305
+ };
306
+
307
+ // src/rules/built-in/crit-data-exfiltration.ts
308
+ var SYSTEM_INFO_PATTERNS = [
309
+ { name: "os.hostname", pattern: /os\.hostname\s*\(\)/g },
310
+ { name: "os.userInfo", pattern: /os\.userInfo\s*\(\)/g },
311
+ { name: "os.platform", pattern: /os\.platform\s*\(\)/g },
312
+ { name: "os.arch", pattern: /os\.arch\s*\(\)/g },
313
+ { name: "os.networkInterfaces", pattern: /os\.networkInterfaces\s*\(\)/g },
314
+ { name: "os.cpus", pattern: /os\.cpus\s*\(\)/g },
315
+ { name: "os.homedir", pattern: /os\.homedir\s*\(\)/g },
316
+ { name: "process.env", pattern: /process\.env(?:\[|\.)/g }
317
+ ];
318
+ var HTTP_TO_IP_PATTERN = /(?:https?\.request|fetch|axios\.(?:get|post|put|request))\s*\(\s*['"`]https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
319
+ var critDataExfiltration = {
320
+ id: "EG-CRIT-001",
321
+ name: "Data Exfiltration Pattern",
322
+ description: "Detects code that collects system info and sends it to external servers via IP address",
323
+ severity: "critical",
324
+ category: "data-exfiltration",
325
+ mitreAttackId: "T1041",
326
+ enabled: true,
327
+ detect(files, _manifest) {
328
+ const evidences = [];
329
+ for (const [filePath, content] of files) {
330
+ if (!filePath.endsWith(".js") && !filePath.endsWith(".ts")) {
331
+ continue;
332
+ }
333
+ const lines = content.split("\n");
334
+ let hasSystemInfo = false;
335
+ let systemInfoPatternName = "";
336
+ let systemInfoLine = 0;
337
+ for (const { name, pattern } of SYSTEM_INFO_PATTERNS) {
338
+ pattern.lastIndex = 0;
339
+ const match = pattern.exec(content);
340
+ if (match) {
341
+ hasSystemInfo = true;
342
+ systemInfoPatternName = name;
343
+ systemInfoLine = content.slice(0, match.index).split("\n").length;
344
+ break;
345
+ }
346
+ }
347
+ HTTP_TO_IP_PATTERN.lastIndex = 0;
348
+ const httpMatch = HTTP_TO_IP_PATTERN.exec(content);
349
+ if (hasSystemInfo && httpMatch) {
350
+ const httpToIpLine = content.slice(0, httpMatch.index).split("\n").length;
351
+ evidences.push({
352
+ filePath,
353
+ lineNumber: httpToIpLine,
354
+ lineContent: lines[httpToIpLine - 1]?.trim(),
355
+ matchedPattern: `${systemInfoPatternName} + http-to-ip`,
356
+ snippet: `System info (${systemInfoPatternName}) collected at line ${systemInfoLine}, sent to IP at line ${httpToIpLine}`
357
+ });
358
+ }
359
+ }
360
+ return evidences;
361
+ }
362
+ };
363
+
364
+ // src/rules/built-in/crit-remote-execution.ts
365
+ var DANGEROUS_PATTERNS = [
366
+ { name: "eval", pattern: /\beval\s*\(/g },
367
+ { name: "Function-constructor", pattern: /new\s+Function\s*\(/g },
368
+ { name: "child_process-exec", pattern: /(?:require\s*\(\s*['"]child_process['"]\s*\)|child_process)\.exec\s*\(/g },
369
+ { name: "child_process-execSync", pattern: /(?:require\s*\(\s*['"]child_process['"]\s*\)|child_process)\.execSync\s*\(/g },
370
+ { name: "child_process-spawn-shell", pattern: /\.spawn\s*\([^)]*\{[^}]*shell\s*:\s*true/g },
371
+ { name: "vm-runInContext", pattern: /vm\.run(?:InContext|InNewContext|InThisContext)\s*\(/g },
372
+ { name: "vm-Script", pattern: /new\s+vm\.Script\s*\(/g }
373
+ ];
374
+ var DYNAMIC_REQUIRE = /require\s*\(\s*(?:[^'"`\s)]|`[^`]*\$\{)/g;
375
+ var critRemoteExecution = {
376
+ id: "EG-CRIT-002",
377
+ name: "Remote Code Execution",
378
+ description: "Detects dangerous code execution patterns like eval, exec, or dynamic require",
379
+ severity: "critical",
380
+ category: "remote-code-execution",
381
+ mitreAttackId: "T1059",
382
+ enabled: true,
383
+ detect(files, _manifest) {
384
+ const evidences = [];
385
+ for (const [filePath, content] of files) {
386
+ if (!filePath.endsWith(".js") && !filePath.endsWith(".ts")) {
387
+ continue;
388
+ }
389
+ const lines = content.split("\n");
390
+ for (const { name, pattern } of DANGEROUS_PATTERNS) {
391
+ pattern.lastIndex = 0;
392
+ let match2;
393
+ while ((match2 = pattern.exec(content)) !== null) {
394
+ const lineNumber = content.slice(0, match2.index).split("\n").length;
395
+ evidences.push({
396
+ filePath,
397
+ lineNumber,
398
+ lineContent: lines[lineNumber - 1]?.trim(),
399
+ matchedPattern: name,
400
+ snippet: match2[0]
401
+ });
402
+ }
403
+ }
404
+ DYNAMIC_REQUIRE.lastIndex = 0;
405
+ let match;
406
+ while ((match = DYNAMIC_REQUIRE.exec(content)) !== null) {
407
+ const lineNumber = content.slice(0, match.index).split("\n").length;
408
+ evidences.push({
409
+ filePath,
410
+ lineNumber,
411
+ lineContent: lines[lineNumber - 1]?.trim(),
412
+ matchedPattern: "dynamic-require",
413
+ snippet: match[0]
414
+ });
415
+ }
416
+ }
417
+ return evidences;
418
+ }
419
+ };
420
+
421
+ // src/rules/built-in/crit-credential-access.ts
422
+ var SENSITIVE_PATHS = [
423
+ { name: "ssh-keys", pattern: /['"`][^'"`]*\.ssh[/\\](?:id_rsa|id_ed25519|id_ecdsa|known_hosts|config|authorized_keys)[^'"`]*['"`]/gi },
424
+ { name: "gnupg", pattern: /['"`][^'"`]*\.gnupg[/\\][^'"`]*['"`]/gi },
425
+ { name: "aws-credentials", pattern: /['"`][^'"`]*\.aws[/\\]credentials[^'"`]*['"`]/gi },
426
+ { name: "azure-config", pattern: /['"`][^'"`]*\.azure[/\\][^'"`]*['"`]/gi },
427
+ { name: "kube-config", pattern: /['"`][^'"`]*\.kube[/\\]config[^'"`]*['"`]/gi },
428
+ { name: "git-credentials", pattern: /['"`][^'"`]*\.git-credentials[^'"`]*['"`]/gi },
429
+ { name: "env-file", pattern: /['"`][^'"`]*\.env(?:\.\w+)?['"`]/gi },
430
+ { name: "npmrc", pattern: /['"`][^'"`]*\.npmrc[^'"`]*['"`]/gi },
431
+ { name: "docker-config", pattern: /['"`][^'"`]*\.docker[/\\]config\.json[^'"`]*['"`]/gi },
432
+ { name: "netrc", pattern: /['"`][^'"`]*\.netrc[^'"`]*['"`]/gi }
433
+ ];
434
+ var FILE_READ_CONTEXT = /(?:readFile|readFileSync|createReadStream|access|accessSync|exists|existsSync|stat|statSync|open|openSync)/;
435
+ var critCredentialAccess = {
436
+ id: "EG-CRIT-003",
437
+ name: "Credential File Access",
438
+ description: "Detects attempts to read sensitive credential files like SSH keys, AWS credentials, or .env files",
439
+ severity: "critical",
440
+ category: "credential-theft",
441
+ mitreAttackId: "T1552.004",
442
+ enabled: true,
443
+ detect(files, _manifest) {
444
+ const evidences = [];
445
+ for (const [filePath, content] of files) {
446
+ if (!filePath.endsWith(".js") && !filePath.endsWith(".ts")) {
447
+ continue;
448
+ }
449
+ const lines = content.split("\n");
450
+ for (const { name, pattern } of SENSITIVE_PATHS) {
451
+ pattern.lastIndex = 0;
452
+ let match;
453
+ while ((match = pattern.exec(content)) !== null) {
454
+ const startIndex = Math.max(0, match.index - 200);
455
+ const endIndex = Math.min(content.length, match.index + match[0].length + 200);
456
+ const context = content.slice(startIndex, endIndex);
457
+ if (FILE_READ_CONTEXT.test(context)) {
458
+ const lineNumber = content.slice(0, match.index).split("\n").length;
459
+ evidences.push({
460
+ filePath,
461
+ lineNumber,
462
+ lineContent: lines[lineNumber - 1]?.trim(),
463
+ matchedPattern: name,
464
+ snippet: match[0]
465
+ });
466
+ }
467
+ }
468
+ }
469
+ }
470
+ return evidences;
471
+ }
472
+ };
473
+
474
+ // src/rules/built-in/high-suspicious-network.ts
475
+ var HTTP_TO_IP = /(?:fetch|axios(?:\.(?:get|post|put|delete|request))?|https?\.(?:get|post|request)|XMLHttpRequest)\s*\([^)]*['"`]https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
476
+ var DYNAMIC_URL = /(?:fetch|axios|https?\.request)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+\s*\w)/g;
477
+ var WEBSOCKET_TO_IP = /new\s+WebSocket\s*\(\s*['"`]wss?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/g;
478
+ var UNUSUAL_PORTS = /['"`]https?:\/\/[^'"`:]+:(?!443|80|8080|3000|8443|5000)[0-9]{2,5}/g;
479
+ var highSuspiciousNetwork = {
480
+ id: "EG-HIGH-002",
481
+ name: "Suspicious Network Activity",
482
+ description: "Detects network requests to IP addresses, dynamic URLs, or unusual ports",
483
+ severity: "high",
484
+ category: "suspicious-network",
485
+ mitreAttackId: "T1071",
486
+ enabled: true,
487
+ detect(files, _manifest) {
488
+ const evidences = [];
489
+ for (const [filePath, content] of files) {
490
+ if (!filePath.endsWith(".js") && !filePath.endsWith(".ts")) {
491
+ continue;
492
+ }
493
+ const lines = content.split("\n");
494
+ const patterns = [
495
+ { pattern: HTTP_TO_IP, name: "http-to-ip" },
496
+ { pattern: DYNAMIC_URL, name: "dynamic-url" },
497
+ { pattern: WEBSOCKET_TO_IP, name: "websocket-to-ip" },
498
+ { pattern: UNUSUAL_PORTS, name: "unusual-port" }
499
+ ];
500
+ for (const { pattern, name } of patterns) {
501
+ pattern.lastIndex = 0;
502
+ let match;
503
+ while ((match = pattern.exec(content)) !== null) {
504
+ const lineNumber = content.slice(0, match.index).split("\n").length;
505
+ evidences.push({
506
+ filePath,
507
+ lineNumber,
508
+ lineContent: lines[lineNumber - 1]?.trim(),
509
+ matchedPattern: name,
510
+ snippet: match[0].slice(0, 100)
511
+ });
512
+ }
513
+ }
514
+ }
515
+ return evidences;
516
+ }
517
+ };
518
+
519
+ // src/rules/built-in/high-obfuscated-code.ts
520
+ var MIN_BASE64_LENGTH = 100;
521
+ var BASE64_PATTERN = /['"`]([A-Za-z0-9+/]{4})*([A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?['"`]/g;
522
+ var HEX_PATTERN = /(?:\\x[0-9a-fA-F]{2}){10,}/g;
523
+ var CHAR_CODE_PATTERN = /String\.fromCharCode\s*\(\s*(?:\d+\s*,?\s*){5,}\)/g;
524
+ var UNICODE_ESCAPE_PATTERN = /(?:\\u[0-9a-fA-F]{4}){10,}/g;
525
+ function calculateEntropy(str) {
526
+ const len = str.length;
527
+ if (len === 0) return 0;
528
+ const freq = {};
529
+ for (const char of str) {
530
+ freq[char] = (freq[char] || 0) + 1;
531
+ }
532
+ let entropy = 0;
533
+ for (const count of Object.values(freq)) {
534
+ const p = count / len;
535
+ entropy -= p * Math.log2(p);
536
+ }
537
+ return entropy;
538
+ }
539
+ var highObfuscatedCode = {
540
+ id: "EG-HIGH-001",
541
+ name: "Code Obfuscation Detected",
542
+ description: "Detects heavily obfuscated code patterns that may hide malicious behavior",
543
+ severity: "high",
544
+ category: "code-obfuscation",
545
+ mitreAttackId: "T1027",
546
+ enabled: true,
547
+ detect(files, _manifest) {
548
+ const evidences = [];
549
+ for (const [filePath, content] of files) {
550
+ if (!filePath.endsWith(".js") && !filePath.endsWith(".ts")) {
551
+ continue;
552
+ }
553
+ const lines = content.split("\n");
554
+ BASE64_PATTERN.lastIndex = 0;
555
+ let match;
556
+ while ((match = BASE64_PATTERN.exec(content)) !== null) {
557
+ const base64Content = match[0].slice(1, -1);
558
+ if (base64Content.length >= MIN_BASE64_LENGTH) {
559
+ const lineNumber = content.slice(0, match.index).split("\n").length;
560
+ evidences.push({
561
+ filePath,
562
+ lineNumber,
563
+ lineContent: (lines[lineNumber - 1]?.trim() || "").slice(0, 80) + "...",
564
+ matchedPattern: "large-base64",
565
+ snippet: `Base64 string of ${base64Content.length} characters`
566
+ });
567
+ }
568
+ }
569
+ HEX_PATTERN.lastIndex = 0;
570
+ while ((match = HEX_PATTERN.exec(content)) !== null) {
571
+ const lineNumber = content.slice(0, match.index).split("\n").length;
572
+ evidences.push({
573
+ filePath,
574
+ lineNumber,
575
+ lineContent: (lines[lineNumber - 1]?.trim() || "").slice(0, 80) + "...",
576
+ matchedPattern: "hex-encoded",
577
+ snippet: match[0].slice(0, 50) + "..."
578
+ });
579
+ }
580
+ CHAR_CODE_PATTERN.lastIndex = 0;
581
+ while ((match = CHAR_CODE_PATTERN.exec(content)) !== null) {
582
+ const lineNumber = content.slice(0, match.index).split("\n").length;
583
+ evidences.push({
584
+ filePath,
585
+ lineNumber,
586
+ lineContent: lines[lineNumber - 1]?.trim(),
587
+ matchedPattern: "charcode-obfuscation",
588
+ snippet: match[0].slice(0, 80)
589
+ });
590
+ }
591
+ UNICODE_ESCAPE_PATTERN.lastIndex = 0;
592
+ while ((match = UNICODE_ESCAPE_PATTERN.exec(content)) !== null) {
593
+ const lineNumber = content.slice(0, match.index).split("\n").length;
594
+ evidences.push({
595
+ filePath,
596
+ lineNumber,
597
+ lineContent: (lines[lineNumber - 1]?.trim() || "").slice(0, 80) + "...",
598
+ matchedPattern: "unicode-escape",
599
+ snippet: match[0].slice(0, 50) + "..."
600
+ });
601
+ }
602
+ if (content.length > 5e3) {
603
+ const entropy = calculateEntropy(content);
604
+ if (entropy > 5.8) {
605
+ evidences.push({
606
+ filePath,
607
+ lineNumber: 1,
608
+ matchedPattern: "high-entropy",
609
+ snippet: `File entropy: ${entropy.toFixed(2)} (threshold: 5.8)`
610
+ });
611
+ }
612
+ }
613
+ }
614
+ return evidences;
615
+ }
616
+ };
617
+
618
+ // src/rules/built-in/high-hardcoded-secret.ts
619
+ var MIN_SECRET_LENGTH = 8;
620
+ var PLACEHOLDER_PATTERNS = [
621
+ /^your[_-]?/i,
622
+ /^<[^>]+>$/,
623
+ /^replace[_-]?me$/i,
624
+ /^xxx+$/i,
625
+ /^x{3,}[_-]x{3,}/i,
626
+ /^todo$/i,
627
+ /^fixme$/i,
628
+ /^example$/i,
629
+ /^placeholder$/i,
630
+ /^changeme$/i,
631
+ /^\*+$/,
632
+ /^\.+$/,
633
+ /^test$/i,
634
+ /^demo$/i
635
+ ];
636
+ var EXCLUDED_FILE_PATTERNS = [
637
+ /\.test\.[jt]sx?$/,
638
+ /\.spec\.[jt]sx?$/,
639
+ /__tests__\//,
640
+ /(?:^|\/)test\//,
641
+ /(?:^|\/)tests\//,
642
+ /(?:^|\/)examples?\//,
643
+ /(?:^|\/)demo\//,
644
+ /\.md$/,
645
+ /\.txt$/,
646
+ /\.rst$/
647
+ ];
648
+ var CODE_FILE_EXTENSIONS = [".js", ".ts", ".jsx", ".tsx", ".mjs", ".cjs"];
649
+ var SECRET_PATTERNS = [
650
+ // AWS Access Key ID (starts with AKIA)
651
+ {
652
+ name: "aws-access-key",
653
+ pattern: /AKIA[0-9A-Z]{16}/g
654
+ },
655
+ // AWS Secret Access Key
656
+ {
657
+ name: "aws-secret-key",
658
+ pattern: /(?:aws[_-]?secret(?:[_-]?access)?[_-]?key|secret[_-]?access[_-]?key)\s*[:=]\s*['"`]([A-Za-z0-9/+=]{40})['"`]/gi,
659
+ matchGroup: 1
660
+ },
661
+ // GitHub tokens (ghp_, gho_, ghs_, ghr_)
662
+ {
663
+ name: "github-token",
664
+ pattern: /gh[pors]_[A-Za-z0-9]{36,}/g
665
+ },
666
+ // Slack tokens (xoxb-, xoxp-, xoxa-, xoxr-)
667
+ {
668
+ name: "slack-token",
669
+ pattern: /xox[bpar]-[0-9]+-[0-9]+-[A-Za-z0-9]+/g
670
+ },
671
+ // Private keys
672
+ {
673
+ name: "private-key",
674
+ pattern: /-----BEGIN\s+(?:RSA\s+|EC\s+|DSA\s+|OPENSSH\s+)?PRIVATE\s+KEY-----/g
675
+ },
676
+ // Bearer tokens (JWT format)
677
+ {
678
+ name: "bearer-token",
679
+ pattern: /Bearer\s+([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/g,
680
+ matchGroup: 1
681
+ },
682
+ // API key assignments
683
+ {
684
+ name: "api-key",
685
+ pattern: /(?:api[_-]?key|apikey)\s*[:=]\s*['"`]([A-Za-z0-9_-]{16,})['"`]/gi,
686
+ matchGroup: 1
687
+ },
688
+ // Generic secrets (password, secret, token, passwd, pwd)
689
+ {
690
+ name: "generic-secret",
691
+ pattern: /(?:password|passwd|pwd|secret|token)\s*[:=]\s*['"`]([^'"`]{8,})['"`]/gi,
692
+ matchGroup: 1
693
+ }
694
+ ];
695
+ function isCodeFile(filePath) {
696
+ return CODE_FILE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
697
+ }
698
+ function isExcludedFile(filePath) {
699
+ return EXCLUDED_FILE_PATTERNS.some((pattern) => pattern.test(filePath));
700
+ }
701
+ function isPlaceholder(value) {
702
+ return PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(value));
703
+ }
704
+ function isInComment(content, matchIndex) {
705
+ const lineStart = content.lastIndexOf("\n", matchIndex) + 1;
706
+ const lineContent = content.slice(lineStart, matchIndex);
707
+ if (lineContent.includes("//")) {
708
+ return true;
709
+ }
710
+ const beforeMatch = content.slice(0, matchIndex);
711
+ const lastBlockStart = beforeMatch.lastIndexOf("/*");
712
+ const lastBlockEnd = beforeMatch.lastIndexOf("*/");
713
+ if (lastBlockStart > lastBlockEnd) {
714
+ return true;
715
+ }
716
+ return false;
717
+ }
718
+ function getLineNumber(content, index) {
719
+ return content.slice(0, index).split("\n").length;
720
+ }
721
+ var highHardcodedSecret = {
722
+ id: "EG-HIGH-006",
723
+ name: "Hardcoded Secrets",
724
+ description: "Detects hardcoded API keys, tokens, passwords, and other secrets in code",
725
+ severity: "high",
726
+ category: "hardcoded-secret",
727
+ mitreAttackId: "T1552.001",
728
+ enabled: true,
729
+ detect(files, _manifest) {
730
+ const evidences = [];
731
+ for (const [filePath, content] of files) {
732
+ if (!isCodeFile(filePath)) {
733
+ continue;
734
+ }
735
+ if (isExcludedFile(filePath)) {
736
+ continue;
737
+ }
738
+ if (!content || content.trim().length === 0) {
739
+ continue;
740
+ }
741
+ const lines = content.split("\n");
742
+ for (const secretPattern of SECRET_PATTERNS) {
743
+ secretPattern.pattern.lastIndex = 0;
744
+ let match;
745
+ while ((match = secretPattern.pattern.exec(content)) !== null) {
746
+ const matchIndex = match.index;
747
+ if (isInComment(content, matchIndex)) {
748
+ continue;
749
+ }
750
+ const secretValue = secretPattern.matchGroup !== void 0 ? match[secretPattern.matchGroup] : match[0];
751
+ if (!secretValue) {
752
+ continue;
753
+ }
754
+ if (secretPattern.name === "generic-secret" && secretValue.length < MIN_SECRET_LENGTH) {
755
+ continue;
756
+ }
757
+ if (isPlaceholder(secretValue)) {
758
+ continue;
759
+ }
760
+ const lineNumber = getLineNumber(content, matchIndex);
761
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
762
+ evidences.push({
763
+ filePath,
764
+ lineNumber,
765
+ lineContent: lineContent.length > 100 ? lineContent.slice(0, 100) + "..." : lineContent,
766
+ matchedPattern: secretPattern.name,
767
+ snippet: `Detected ${secretPattern.name}: ${secretValue.slice(0, 20)}${secretValue.length > 20 ? "..." : ""}`
768
+ });
769
+ }
770
+ }
771
+ }
772
+ return evidences;
773
+ }
774
+ };
775
+
776
+ // src/rules/built-in/med-excessive-activation.ts
777
+ var medExcessiveActivation = {
778
+ id: "EG-MED-001",
779
+ name: "Excessive Activation Events",
780
+ description: 'Extension uses "*" activation event which means it activates on every action, potentially for surveillance',
781
+ severity: "medium",
782
+ category: "excessive-permission",
783
+ enabled: true,
784
+ detect(_files, manifest) {
785
+ const evidences = [];
786
+ const activationEvents = manifest.activationEvents ?? [];
787
+ if (activationEvents.includes("*")) {
788
+ evidences.push({
789
+ filePath: "package.json",
790
+ lineNumber: 1,
791
+ matchedPattern: "activation-star",
792
+ snippet: 'activationEvents: ["*"]',
793
+ lineContent: "Extension activates on every VS Code action"
794
+ });
795
+ }
796
+ if (activationEvents.includes("onStartupFinished")) {
797
+ evidences.push({
798
+ filePath: "package.json",
799
+ lineNumber: 1,
800
+ matchedPattern: "activation-startup",
801
+ snippet: 'activationEvents: ["onStartupFinished"]',
802
+ lineContent: "Extension activates immediately on VS Code startup"
803
+ });
804
+ }
805
+ return evidences;
806
+ }
807
+ };
808
+
809
+ // src/rules/built-in/index.ts
810
+ function registerBuiltInRules() {
811
+ ruleRegistry.register(critDataExfiltration);
812
+ ruleRegistry.register(critRemoteExecution);
813
+ ruleRegistry.register(critCredentialAccess);
814
+ ruleRegistry.register(highSuspiciousNetwork);
815
+ ruleRegistry.register(highObfuscatedCode);
816
+ ruleRegistry.register(highHardcodedSecret);
817
+ ruleRegistry.register(medExcessiveActivation);
818
+ }
819
+
820
+ // src/scanner/scanner.ts
821
+ var rulesRegistered = false;
822
+ function ensureRulesRegistered() {
823
+ if (!rulesRegistered) {
824
+ registerBuiltInRules();
825
+ rulesRegistered = true;
826
+ }
827
+ }
828
+ var DEFAULT_OPTIONS = {
829
+ idePaths: [],
830
+ autoDetect: true,
831
+ severity: "info",
832
+ rules: [],
833
+ skipRules: [],
834
+ concurrency: 4,
835
+ timeout: 3e4
836
+ };
837
+ var SEVERITY_PENALTY = {
838
+ critical: 35,
839
+ high: 18,
840
+ medium: 8,
841
+ low: 3,
842
+ info: 1
843
+ };
844
+ var ExtensionGuardScanner = class {
845
+ options;
846
+ ruleEngine;
847
+ constructor(options) {
848
+ ensureRulesRegistered();
849
+ this.options = { ...DEFAULT_OPTIONS, ...options };
850
+ this.ruleEngine = new RuleEngine({
851
+ rules: this.options.rules.length > 0 ? this.options.rules : void 0,
852
+ skipRules: this.options.skipRules.length > 0 ? this.options.skipRules : void 0,
853
+ minSeverity: this.options.severity
854
+ });
855
+ }
856
+ async scan(options) {
857
+ const startTime = Date.now();
858
+ const mergedOptions = { ...this.options, ...options };
859
+ let ides;
860
+ if (mergedOptions.autoDetect && mergedOptions.idePaths.length === 0) {
861
+ ides = detectIDEPaths();
862
+ } else {
863
+ ides = mergedOptions.idePaths.map((p) => ({
864
+ name: "Custom",
865
+ path: p,
866
+ extensionCount: 0
867
+ }));
868
+ }
869
+ const allExtensions = await Promise.all(
870
+ ides.map((ide) => readExtensionsFromDirectory(ide.path))
871
+ );
872
+ const extensionMap = /* @__PURE__ */ new Map();
873
+ for (let i = 0; i < ides.length; i++) {
874
+ const ide = ides[i];
875
+ for (const ext of allExtensions[i]) {
876
+ if (!extensionMap.has(ext.id)) {
877
+ extensionMap.set(ext.id, { ide, ext });
878
+ }
879
+ }
880
+ ide.extensionCount = allExtensions[i].length;
881
+ }
882
+ const results = [];
883
+ for (const { ext } of extensionMap.values()) {
884
+ const result = await this.scanExtension(ext);
885
+ results.push(result);
886
+ }
887
+ const summary = this.calculateSummary(results);
888
+ return {
889
+ scanId: randomUUID2(),
890
+ version: VERSION,
891
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
892
+ environment: {
893
+ os: `${os2.platform()} ${os2.release()}`,
894
+ ides
895
+ },
896
+ totalExtensions: Array.from(allExtensions).reduce((sum, arr) => sum + arr.length, 0),
897
+ uniqueExtensions: extensionMap.size,
898
+ results,
899
+ summary,
900
+ scanDurationMs: Date.now() - startTime
901
+ };
902
+ }
903
+ async scanExtension(ext) {
904
+ const startTime = Date.now();
905
+ const files = await collectFiles(ext.installPath);
906
+ const manifestContent = files.get("package.json");
907
+ let manifest = {
908
+ name: ext.id.split(".")[1] || ext.id,
909
+ publisher: ext.publisher.name,
910
+ version: ext.version
911
+ };
912
+ if (manifestContent) {
913
+ try {
914
+ manifest = JSON.parse(manifestContent);
915
+ } catch {
916
+ }
917
+ }
918
+ const findings = this.ruleEngine.run(files, manifest);
919
+ const trustScore = this.calculateTrustScore(findings);
920
+ const riskLevel = this.calculateRiskLevel(trustScore, findings);
921
+ return {
922
+ extensionId: ext.id,
923
+ displayName: ext.displayName,
924
+ version: ext.version,
925
+ trustScore,
926
+ riskLevel,
927
+ findings,
928
+ metadata: ext,
929
+ analyzedFiles: files.size,
930
+ scanDurationMs: Date.now() - startTime
931
+ };
932
+ }
933
+ calculateTrustScore(findings) {
934
+ let score = 100;
935
+ for (const finding of findings) {
936
+ score -= SEVERITY_PENALTY[finding.severity];
937
+ }
938
+ return Math.max(0, Math.min(100, score));
939
+ }
940
+ calculateRiskLevel(trustScore, findings) {
941
+ if (findings.some((f) => f.severity === "critical")) {
942
+ return "critical";
943
+ }
944
+ if (findings.some((f) => f.severity === "high")) {
945
+ return "high";
946
+ }
947
+ if (trustScore >= 90) return "safe";
948
+ if (trustScore >= 70) return "low";
949
+ if (trustScore >= 45) return "medium";
950
+ if (trustScore >= 20) return "high";
951
+ return "critical";
952
+ }
953
+ calculateSummary(results) {
954
+ const byRiskLevel = {
955
+ critical: 0,
956
+ high: 0,
957
+ medium: 0,
958
+ low: 0,
959
+ safe: 0
960
+ };
961
+ const bySeverity = {
962
+ critical: 0,
963
+ high: 0,
964
+ medium: 0,
965
+ low: 0,
966
+ info: 0
967
+ };
968
+ const byCategory = {};
969
+ for (const result of results) {
970
+ byRiskLevel[result.riskLevel]++;
971
+ for (const finding of result.findings) {
972
+ bySeverity[finding.severity]++;
973
+ byCategory[finding.category] = (byCategory[finding.category] ?? 0) + 1;
974
+ }
975
+ }
976
+ const allFindings = results.flatMap((r) => r.findings);
977
+ const severityOrder = {
978
+ critical: 0,
979
+ high: 1,
980
+ medium: 2,
981
+ low: 3,
982
+ info: 4
983
+ };
984
+ const topFindings = allFindings.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]).slice(0, 10);
985
+ const totalExtensions = results.length;
986
+ const safeCount = byRiskLevel.safe + byRiskLevel.low;
987
+ const overallHealthScore = totalExtensions > 0 ? Math.round(safeCount / totalExtensions * 100) : 100;
988
+ return {
989
+ byRiskLevel,
990
+ bySeverity,
991
+ byCategory,
992
+ topFindings,
993
+ overallHealthScore
994
+ };
995
+ }
996
+ };
997
+
998
+ // src/reporter/json-reporter.ts
999
+ var SEVERITY_ORDER2 = {
1000
+ critical: 0,
1001
+ high: 1,
1002
+ medium: 2,
1003
+ low: 3,
1004
+ info: 4
1005
+ };
1006
+ var JsonReporter = class {
1007
+ format = "json";
1008
+ generate(report, options = {}) {
1009
+ const {
1010
+ includeEvidence = true,
1011
+ includeSafe = true,
1012
+ minSeverity = "info"
1013
+ } = options;
1014
+ const filteredResults = this.filterResults(report.results, {
1015
+ includeSafe,
1016
+ minSeverity,
1017
+ includeEvidence
1018
+ });
1019
+ const output = {
1020
+ ...report,
1021
+ results: filteredResults
1022
+ };
1023
+ return JSON.stringify(output, null, 2);
1024
+ }
1025
+ filterResults(results, options) {
1026
+ let filtered = results;
1027
+ if (!options.includeSafe) {
1028
+ filtered = filtered.filter((r) => r.riskLevel !== "safe" && r.riskLevel !== "low");
1029
+ }
1030
+ const minOrder = SEVERITY_ORDER2[options.minSeverity];
1031
+ return filtered.map((result) => {
1032
+ const filteredFindings = result.findings.filter(
1033
+ (f) => SEVERITY_ORDER2[f.severity] <= minOrder
1034
+ );
1035
+ const findingsOutput = options.includeEvidence ? filteredFindings : filteredFindings.map(({ evidence, ...rest }) => rest);
1036
+ return {
1037
+ ...result,
1038
+ findings: findingsOutput
1039
+ };
1040
+ });
1041
+ }
1042
+ };
1043
+
1044
+ // src/reporter/sarif-reporter.ts
1045
+ var SEVERITY_TO_LEVEL = {
1046
+ critical: "error",
1047
+ high: "error",
1048
+ medium: "warning",
1049
+ low: "note",
1050
+ info: "note"
1051
+ };
1052
+ var SEVERITY_TO_SCORE = {
1053
+ critical: "9.0",
1054
+ high: "7.0",
1055
+ medium: "5.0",
1056
+ low: "3.0",
1057
+ info: "1.0"
1058
+ };
1059
+ var SarifReporter = class {
1060
+ format = "sarif";
1061
+ generate(report, _options) {
1062
+ const rules = this.extractRules(report);
1063
+ const results = this.extractResults(report, rules);
1064
+ const sarif = {
1065
+ $schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
1066
+ version: "2.1.0",
1067
+ runs: [
1068
+ {
1069
+ tool: {
1070
+ driver: {
1071
+ name: "Extension Guard",
1072
+ version: report.version,
1073
+ informationUri: "https://github.com/aspect-guard/extension-guard",
1074
+ rules
1075
+ }
1076
+ },
1077
+ results
1078
+ }
1079
+ ]
1080
+ };
1081
+ return JSON.stringify(sarif, null, 2);
1082
+ }
1083
+ extractRules(report) {
1084
+ const ruleMap = /* @__PURE__ */ new Map();
1085
+ for (const result of report.results) {
1086
+ for (const finding of result.findings) {
1087
+ if (!ruleMap.has(finding.ruleId)) {
1088
+ ruleMap.set(finding.ruleId, {
1089
+ id: finding.ruleId,
1090
+ name: finding.title,
1091
+ shortDescription: { text: finding.title },
1092
+ fullDescription: { text: finding.description },
1093
+ defaultConfiguration: { level: SEVERITY_TO_LEVEL[finding.severity] || "note" },
1094
+ properties: {
1095
+ "security-severity": SEVERITY_TO_SCORE[finding.severity] || "1.0",
1096
+ tags: ["security", finding.category]
1097
+ }
1098
+ });
1099
+ }
1100
+ }
1101
+ }
1102
+ return Array.from(ruleMap.values());
1103
+ }
1104
+ extractResults(report, rules) {
1105
+ const results = [];
1106
+ const ruleIndexMap = new Map(rules.map((r, i) => [r.id, i]));
1107
+ for (const scanResult of report.results) {
1108
+ for (const finding of scanResult.findings) {
1109
+ const ruleIndex = ruleIndexMap.get(finding.ruleId) ?? 0;
1110
+ results.push({
1111
+ ruleId: finding.ruleId,
1112
+ ruleIndex,
1113
+ level: SEVERITY_TO_LEVEL[finding.severity] || "note",
1114
+ message: {
1115
+ text: `${finding.title} in ${scanResult.extensionId}: ${finding.description}`
1116
+ },
1117
+ locations: [
1118
+ {
1119
+ physicalLocation: {
1120
+ artifactLocation: {
1121
+ uri: `${scanResult.extensionId}/${finding.evidence.filePath}`
1122
+ },
1123
+ region: finding.evidence.lineNumber ? {
1124
+ startLine: finding.evidence.lineNumber,
1125
+ startColumn: finding.evidence.columnNumber
1126
+ } : void 0
1127
+ }
1128
+ }
1129
+ ],
1130
+ properties: {
1131
+ extensionId: scanResult.extensionId,
1132
+ trustScore: scanResult.trustScore,
1133
+ mitreAttackId: finding.mitreAttackId
1134
+ }
1135
+ });
1136
+ }
1137
+ }
1138
+ return results;
1139
+ }
1140
+ };
1141
+
1142
+ // src/reporter/markdown-reporter.ts
1143
+ var SEVERITY_EMOJI = {
1144
+ critical: "\u{1F534}",
1145
+ high: "\u{1F7E0}",
1146
+ medium: "\u{1F7E1}",
1147
+ low: "\u{1F7E2}",
1148
+ info: "\u26AA"
1149
+ };
1150
+ var RISK_EMOJI = {
1151
+ critical: "\u26D4",
1152
+ high: "\u{1F534}",
1153
+ medium: "\u{1F7E1}",
1154
+ low: "\u{1F7E2}",
1155
+ safe: "\u2705"
1156
+ };
1157
+ var MarkdownReporter = class {
1158
+ format = "markdown";
1159
+ generate(report, options = {}) {
1160
+ const { includeSafe = false } = options;
1161
+ const lines = [];
1162
+ lines.push("# Extension Guard Scan Report");
1163
+ lines.push("");
1164
+ lines.push(`**Scan ID:** \`${report.scanId}\``);
1165
+ lines.push(`**Date:** ${new Date(report.timestamp).toLocaleString()}`);
1166
+ lines.push(`**Extension Guard Version:** ${report.version}`);
1167
+ lines.push("");
1168
+ lines.push("## Environment");
1169
+ lines.push("");
1170
+ lines.push(`- **OS:** ${report.environment.os}`);
1171
+ for (const ide of report.environment.ides) {
1172
+ lines.push(`- **${ide.name}:** ${ide.path} (${ide.extensionCount} extensions)`);
1173
+ }
1174
+ lines.push("");
1175
+ lines.push("## Summary");
1176
+ lines.push("");
1177
+ lines.push(`| Metric | Value |`);
1178
+ lines.push(`|--------|-------|`);
1179
+ lines.push(`| Total Extensions | ${report.totalExtensions} |`);
1180
+ lines.push(`| Unique Extensions | ${report.uniqueExtensions} |`);
1181
+ lines.push(`| Health Score | ${report.summary.overallHealthScore}% |`);
1182
+ lines.push(`| Scan Duration | ${(report.scanDurationMs / 1e3).toFixed(2)}s |`);
1183
+ lines.push("");
1184
+ lines.push("### Risk Level Distribution");
1185
+ lines.push("");
1186
+ lines.push("| Risk Level | Count |");
1187
+ lines.push("|------------|-------|");
1188
+ for (const [level, count] of Object.entries(report.summary.byRiskLevel)) {
1189
+ if (count > 0 || includeSafe) {
1190
+ lines.push(`| ${RISK_EMOJI[level] || ""} ${level} | ${count} |`);
1191
+ }
1192
+ }
1193
+ lines.push("");
1194
+ const totalFindings = Object.values(report.summary.bySeverity).reduce((a, b) => a + b, 0);
1195
+ if (totalFindings > 0) {
1196
+ lines.push("### Findings by Severity");
1197
+ lines.push("");
1198
+ lines.push("| Severity | Count |");
1199
+ lines.push("|----------|-------|");
1200
+ for (const [severity, count] of Object.entries(report.summary.bySeverity)) {
1201
+ if (count > 0) {
1202
+ lines.push(`| ${SEVERITY_EMOJI[severity] || ""} ${severity} | ${count} |`);
1203
+ }
1204
+ }
1205
+ lines.push("");
1206
+ }
1207
+ const riskyResults = report.results.filter(
1208
+ (r) => r.riskLevel === "critical" || r.riskLevel === "high"
1209
+ );
1210
+ if (riskyResults.length > 0) {
1211
+ lines.push("## \u26A0\uFE0F High Risk Extensions");
1212
+ lines.push("");
1213
+ for (const result of riskyResults) {
1214
+ lines.push(...this.formatExtensionResult(result));
1215
+ }
1216
+ }
1217
+ const mediumResults = report.results.filter((r) => r.riskLevel === "medium");
1218
+ if (mediumResults.length > 0) {
1219
+ lines.push("## Medium Risk Extensions");
1220
+ lines.push("");
1221
+ for (const result of mediumResults) {
1222
+ lines.push(...this.formatExtensionResult(result));
1223
+ }
1224
+ }
1225
+ if (includeSafe) {
1226
+ const safeResults = report.results.filter(
1227
+ (r) => r.riskLevel === "safe" || r.riskLevel === "low"
1228
+ );
1229
+ if (safeResults.length > 0) {
1230
+ lines.push("## \u2705 Safe Extensions");
1231
+ lines.push("");
1232
+ lines.push("| Extension | Version | Trust Score |");
1233
+ lines.push("|-----------|---------|-------------|");
1234
+ for (const result of safeResults) {
1235
+ lines.push(`| ${result.extensionId} | ${result.version} | ${result.trustScore}/100 |`);
1236
+ }
1237
+ lines.push("");
1238
+ }
1239
+ }
1240
+ lines.push("---");
1241
+ lines.push("");
1242
+ lines.push("*Generated by [Extension Guard](https://github.com/aspect-guard/extension-guard)*");
1243
+ return lines.join("\n");
1244
+ }
1245
+ formatExtensionResult(result) {
1246
+ const lines = [];
1247
+ lines.push(`### ${RISK_EMOJI[result.riskLevel] || ""} ${result.extensionId}`);
1248
+ lines.push("");
1249
+ lines.push(`- **Display Name:** ${result.displayName}`);
1250
+ lines.push(`- **Version:** ${result.version}`);
1251
+ lines.push(`- **Publisher:** ${result.metadata.publisher.name}`);
1252
+ lines.push(`- **Trust Score:** ${result.trustScore}/100`);
1253
+ lines.push(`- **Risk Level:** ${result.riskLevel}`);
1254
+ lines.push("");
1255
+ if (result.findings.length > 0) {
1256
+ lines.push("#### Findings");
1257
+ lines.push("");
1258
+ for (const finding of result.findings) {
1259
+ lines.push(...this.formatFinding(finding));
1260
+ }
1261
+ }
1262
+ return lines;
1263
+ }
1264
+ formatFinding(finding) {
1265
+ const lines = [];
1266
+ const emoji = SEVERITY_EMOJI[finding.severity] || "";
1267
+ lines.push(`- ${emoji} **${finding.severity.toUpperCase()}:** ${finding.title}`);
1268
+ lines.push(` - ${finding.description}`);
1269
+ if (finding.evidence.filePath) {
1270
+ const location = finding.evidence.lineNumber ? `${finding.evidence.filePath}:${finding.evidence.lineNumber}` : finding.evidence.filePath;
1271
+ lines.push(` - \u{1F4CD} Location: \`${location}\``);
1272
+ }
1273
+ if (finding.mitreAttackId) {
1274
+ lines.push(` - \u{1F3AF} MITRE ATT&CK: ${finding.mitreAttackId}`);
1275
+ }
1276
+ lines.push("");
1277
+ return lines;
1278
+ }
1279
+ };
1280
+
1281
+ // src/policy/policy-loader.ts
1282
+ import * as fs4 from "fs/promises";
1283
+ import * as path4 from "path";
1284
+ var DEFAULT_CONFIG_NAME = ".extension-guard.json";
1285
+ async function loadPolicyConfig(configPath) {
1286
+ const resolvedPath = configPath ?? path4.join(process.cwd(), DEFAULT_CONFIG_NAME);
1287
+ try {
1288
+ const content = await fs4.readFile(resolvedPath, "utf-8");
1289
+ const config = JSON.parse(content);
1290
+ validatePolicyConfig(config);
1291
+ return config;
1292
+ } catch (error) {
1293
+ if (isNodeError(error) && error.code === "ENOENT") {
1294
+ return null;
1295
+ }
1296
+ throw error;
1297
+ }
1298
+ }
1299
+ function validatePolicyConfig(config) {
1300
+ if (typeof config !== "object" || config === null) {
1301
+ throw new Error("Policy config must be an object");
1302
+ }
1303
+ const obj = config;
1304
+ if (typeof obj.version !== "string") {
1305
+ throw new Error('Policy config must have a "version" field of type string');
1306
+ }
1307
+ if (obj.scanning !== void 0) {
1308
+ validateScanningConfig(obj.scanning);
1309
+ }
1310
+ if (obj.policy !== void 0) {
1311
+ validatePolicySection(obj.policy);
1312
+ }
1313
+ }
1314
+ function validateScanningConfig(scanning) {
1315
+ if (typeof scanning !== "object" || scanning === null) {
1316
+ throw new Error("scanning must be an object");
1317
+ }
1318
+ const obj = scanning;
1319
+ if (obj.minSeverity !== void 0) {
1320
+ const validSeverities = ["critical", "high", "medium", "low", "info"];
1321
+ if (!validSeverities.includes(obj.minSeverity)) {
1322
+ throw new Error(`scanning.minSeverity must be one of: ${validSeverities.join(", ")}`);
1323
+ }
1324
+ }
1325
+ if (obj.skipRules !== void 0) {
1326
+ if (!Array.isArray(obj.skipRules) || !obj.skipRules.every((r) => typeof r === "string")) {
1327
+ throw new Error("scanning.skipRules must be an array of strings");
1328
+ }
1329
+ }
1330
+ if (obj.timeout !== void 0) {
1331
+ if (typeof obj.timeout !== "number" || obj.timeout <= 0) {
1332
+ throw new Error("scanning.timeout must be a positive number");
1333
+ }
1334
+ }
1335
+ }
1336
+ function validatePolicySection(policy) {
1337
+ if (typeof policy !== "object" || policy === null) {
1338
+ throw new Error("policy must be an object");
1339
+ }
1340
+ const obj = policy;
1341
+ if (obj.allowlist !== void 0) {
1342
+ if (!Array.isArray(obj.allowlist) || !obj.allowlist.every((id) => typeof id === "string")) {
1343
+ throw new Error("policy.allowlist must be an array of extension ID strings");
1344
+ }
1345
+ }
1346
+ if (obj.blocklist !== void 0) {
1347
+ if (!Array.isArray(obj.blocklist) || !obj.blocklist.every((id) => typeof id === "string")) {
1348
+ throw new Error("policy.blocklist must be an array of extension ID strings");
1349
+ }
1350
+ }
1351
+ if (obj.rules !== void 0) {
1352
+ validatePolicyRules(obj.rules);
1353
+ }
1354
+ }
1355
+ function validatePolicyRules(rules) {
1356
+ if (typeof rules !== "object" || rules === null) {
1357
+ throw new Error("policy.rules must be an object");
1358
+ }
1359
+ const obj = rules;
1360
+ const validActions = ["block", "warn", "info"];
1361
+ if (obj.minTrustScore !== void 0) {
1362
+ const rule = obj.minTrustScore;
1363
+ if (typeof rule.threshold !== "number" || rule.threshold < 0 || rule.threshold > 100) {
1364
+ throw new Error("minTrustScore.threshold must be a number between 0 and 100");
1365
+ }
1366
+ if (!validActions.includes(rule.action)) {
1367
+ throw new Error(`minTrustScore.action must be one of: ${validActions.join(", ")}`);
1368
+ }
1369
+ }
1370
+ if (obj.requireVerifiedPublisher !== void 0) {
1371
+ const rule = obj.requireVerifiedPublisher;
1372
+ if (typeof rule.enabled !== "boolean") {
1373
+ throw new Error("requireVerifiedPublisher.enabled must be a boolean");
1374
+ }
1375
+ if (!validActions.includes(rule.action)) {
1376
+ throw new Error(`requireVerifiedPublisher.action must be one of: ${validActions.join(", ")}`);
1377
+ }
1378
+ if (rule.exceptions !== void 0) {
1379
+ if (!Array.isArray(rule.exceptions) || !rule.exceptions.every((e) => typeof e === "string")) {
1380
+ throw new Error("requireVerifiedPublisher.exceptions must be an array of strings");
1381
+ }
1382
+ }
1383
+ }
1384
+ if (obj.maxDaysSinceUpdate !== void 0) {
1385
+ const rule = obj.maxDaysSinceUpdate;
1386
+ if (typeof rule.days !== "number" || rule.days <= 0) {
1387
+ throw new Error("maxDaysSinceUpdate.days must be a positive number");
1388
+ }
1389
+ if (!validActions.includes(rule.action)) {
1390
+ throw new Error(`maxDaysSinceUpdate.action must be one of: ${validActions.join(", ")}`);
1391
+ }
1392
+ }
1393
+ if (obj.blockObfuscated !== void 0) {
1394
+ const rule = obj.blockObfuscated;
1395
+ if (typeof rule.enabled !== "boolean") {
1396
+ throw new Error("blockObfuscated.enabled must be a boolean");
1397
+ }
1398
+ if (!validActions.includes(rule.action)) {
1399
+ throw new Error(`blockObfuscated.action must be one of: ${validActions.join(", ")}`);
1400
+ }
1401
+ }
1402
+ }
1403
+ function isNodeError(error) {
1404
+ return error instanceof Error && "code" in error;
1405
+ }
1406
+
1407
+ // src/policy/policy-engine.ts
1408
+ var OBFUSCATION_RULE_ID = "EG-HIGH-001";
1409
+ var MS_PER_DAY = 1e3 * 60 * 60 * 24;
1410
+ var PolicyEngine = class {
1411
+ /**
1412
+ * Create a new PolicyEngine instance.
1413
+ *
1414
+ * @param config - The policy configuration to evaluate against
1415
+ */
1416
+ constructor(config) {
1417
+ this.config = config;
1418
+ }
1419
+ violations = [];
1420
+ /**
1421
+ * Evaluate scan results against the configured policy.
1422
+ *
1423
+ * Checks are applied in this order for each extension:
1424
+ * 1. Blocklist - if matched, block immediately and skip other checks
1425
+ * 2. Allowlist - if matched, skip all other checks
1426
+ * 3. Individual rules (minTrustScore, blockObfuscated, etc.)
1427
+ *
1428
+ * @param results - Array of scan results to evaluate
1429
+ * @returns Array of policy violations found
1430
+ */
1431
+ evaluate(results) {
1432
+ this.violations = [];
1433
+ for (const result of results) {
1434
+ const extId = result.extensionId;
1435
+ if (this.config.policy?.blocklist?.includes(extId)) {
1436
+ this.violations.push({
1437
+ extensionId: extId,
1438
+ rule: "blocklist",
1439
+ message: "Extension is blocklisted",
1440
+ action: "block"
1441
+ });
1442
+ continue;
1443
+ }
1444
+ if (this.config.policy?.allowlist?.includes(extId)) {
1445
+ continue;
1446
+ }
1447
+ this.checkMinTrustScore(result);
1448
+ this.checkBlockObfuscated(result);
1449
+ this.checkRequireVerifiedPublisher(result);
1450
+ this.checkMaxDaysSinceUpdate(result);
1451
+ }
1452
+ return this.violations;
1453
+ }
1454
+ /**
1455
+ * Check if there are any violations with 'block' action.
1456
+ *
1457
+ * @returns true if any blocking violations exist
1458
+ */
1459
+ hasBlockingViolations() {
1460
+ return this.violations.some((v) => v.action === "block");
1461
+ }
1462
+ /**
1463
+ * Get all violations from the last evaluation.
1464
+ *
1465
+ * @returns Array of all policy violations
1466
+ */
1467
+ getViolations() {
1468
+ return this.violations;
1469
+ }
1470
+ /**
1471
+ * Check minTrustScore rule.
1472
+ */
1473
+ checkMinTrustScore(result) {
1474
+ const rule = this.config.policy?.rules?.minTrustScore;
1475
+ if (!rule) return;
1476
+ if (result.trustScore < rule.threshold) {
1477
+ this.violations.push({
1478
+ extensionId: result.extensionId,
1479
+ rule: "minTrustScore",
1480
+ message: `Trust score ${result.trustScore} below threshold ${rule.threshold}`,
1481
+ action: rule.action
1482
+ });
1483
+ }
1484
+ }
1485
+ /**
1486
+ * Check blockObfuscated rule.
1487
+ */
1488
+ checkBlockObfuscated(result) {
1489
+ const rule = this.config.policy?.rules?.blockObfuscated;
1490
+ if (!rule?.enabled) return;
1491
+ const hasObfuscation = result.findings.some((f) => f.ruleId === OBFUSCATION_RULE_ID);
1492
+ if (hasObfuscation) {
1493
+ this.violations.push({
1494
+ extensionId: result.extensionId,
1495
+ rule: "blockObfuscated",
1496
+ message: "Extension contains obfuscated code",
1497
+ action: rule.action
1498
+ });
1499
+ }
1500
+ }
1501
+ /**
1502
+ * Check requireVerifiedPublisher rule.
1503
+ */
1504
+ checkRequireVerifiedPublisher(result) {
1505
+ const rule = this.config.policy?.rules?.requireVerifiedPublisher;
1506
+ if (!rule?.enabled) return;
1507
+ const metadata = result.metadata;
1508
+ if (!metadata.publisher.verified) {
1509
+ if (rule.exceptions?.includes(result.extensionId)) {
1510
+ return;
1511
+ }
1512
+ this.violations.push({
1513
+ extensionId: result.extensionId,
1514
+ rule: "requireVerifiedPublisher",
1515
+ message: "Extension publisher is not verified",
1516
+ action: rule.action
1517
+ });
1518
+ }
1519
+ }
1520
+ /**
1521
+ * Check maxDaysSinceUpdate rule.
1522
+ */
1523
+ checkMaxDaysSinceUpdate(result) {
1524
+ const rule = this.config.policy?.rules?.maxDaysSinceUpdate;
1525
+ if (!rule) return;
1526
+ const metadata = result.metadata;
1527
+ if (!metadata.lastUpdated) return;
1528
+ const lastUpdatedDate = new Date(metadata.lastUpdated);
1529
+ const daysSinceUpdate = Math.floor((Date.now() - lastUpdatedDate.getTime()) / MS_PER_DAY);
1530
+ if (daysSinceUpdate > rule.days) {
1531
+ this.violations.push({
1532
+ extensionId: result.extensionId,
1533
+ rule: "maxDaysSinceUpdate",
1534
+ message: `Extension not updated in ${daysSinceUpdate} days (max: ${rule.days})`,
1535
+ action: rule.action
1536
+ });
1537
+ }
1538
+ }
1539
+ };
1540
+
1541
+ // src/index.ts
1542
+ var VERSION = "0.1.0";
1543
+ export {
1544
+ ExtensionGuardScanner,
1545
+ IDE_PATHS,
1546
+ JsonReporter,
1547
+ MarkdownReporter,
1548
+ PolicyEngine,
1549
+ RuleEngine,
1550
+ SEVERITY_ORDER,
1551
+ SarifReporter,
1552
+ VERSION,
1553
+ collectFiles,
1554
+ compareSeverity,
1555
+ detectIDEPaths,
1556
+ expandPath,
1557
+ isAtLeastSeverity,
1558
+ loadPolicyConfig,
1559
+ readExtension,
1560
+ readExtensionsFromDirectory,
1561
+ registerBuiltInRules,
1562
+ ruleRegistry,
1563
+ shouldCollectFile
1564
+ };
1565
+ //# sourceMappingURL=index.js.map