@kairosinternational/watchman 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.
Files changed (159) hide show
  1. package/PHASE_3A_COMPLETE.md +63 -0
  2. package/dist/engine.d.ts +9 -0
  3. package/dist/engine.d.ts.map +1 -0
  4. package/dist/engine.js +99 -0
  5. package/dist/engine.js.map +1 -0
  6. package/dist/index.d.ts +12 -0
  7. package/dist/index.d.ts.map +1 -0
  8. package/dist/index.js +21 -0
  9. package/dist/index.js.map +1 -0
  10. package/dist/scanners/ai-tool-integrity/index.d.ts +2 -0
  11. package/dist/scanners/ai-tool-integrity/index.d.ts.map +1 -0
  12. package/dist/scanners/ai-tool-integrity/index.js +6 -0
  13. package/dist/scanners/ai-tool-integrity/index.js.map +1 -0
  14. package/dist/scanners/ai-tool-integrity/patterns/injection-signatures.d.ts +7 -0
  15. package/dist/scanners/ai-tool-integrity/patterns/injection-signatures.d.ts.map +1 -0
  16. package/dist/scanners/ai-tool-integrity/patterns/injection-signatures.js +51 -0
  17. package/dist/scanners/ai-tool-integrity/patterns/injection-signatures.js.map +1 -0
  18. package/dist/scanners/ai-tool-integrity/rules/prompt-injection.rule.d.ts +3 -0
  19. package/dist/scanners/ai-tool-integrity/rules/prompt-injection.rule.d.ts.map +1 -0
  20. package/dist/scanners/ai-tool-integrity/rules/prompt-injection.rule.js +49 -0
  21. package/dist/scanners/ai-tool-integrity/rules/prompt-injection.rule.js.map +1 -0
  22. package/dist/scanners/ai-tool-integrity/rules/tool-manifest-drift.rule.d.ts +3 -0
  23. package/dist/scanners/ai-tool-integrity/rules/tool-manifest-drift.rule.d.ts.map +1 -0
  24. package/dist/scanners/ai-tool-integrity/rules/tool-manifest-drift.rule.js +71 -0
  25. package/dist/scanners/ai-tool-integrity/rules/tool-manifest-drift.rule.js.map +1 -0
  26. package/dist/scanners/ai-tool-integrity/rules/unauthorized-model-call.rule.d.ts +3 -0
  27. package/dist/scanners/ai-tool-integrity/rules/unauthorized-model-call.rule.d.ts.map +1 -0
  28. package/dist/scanners/ai-tool-integrity/rules/unauthorized-model-call.rule.js +56 -0
  29. package/dist/scanners/ai-tool-integrity/rules/unauthorized-model-call.rule.js.map +1 -0
  30. package/dist/scanners/ai-tool-integrity/scanner.d.ts +3 -0
  31. package/dist/scanners/ai-tool-integrity/scanner.d.ts.map +1 -0
  32. package/dist/scanners/ai-tool-integrity/scanner.js +26 -0
  33. package/dist/scanners/ai-tool-integrity/scanner.js.map +1 -0
  34. package/dist/scanners/dependency-integrity/index.d.ts +2 -0
  35. package/dist/scanners/dependency-integrity/index.d.ts.map +1 -0
  36. package/dist/scanners/dependency-integrity/index.js +6 -0
  37. package/dist/scanners/dependency-integrity/index.js.map +1 -0
  38. package/dist/scanners/dependency-integrity/patterns/known-typosquats.d.ts +8 -0
  39. package/dist/scanners/dependency-integrity/patterns/known-typosquats.d.ts.map +1 -0
  40. package/dist/scanners/dependency-integrity/patterns/known-typosquats.js +38 -0
  41. package/dist/scanners/dependency-integrity/patterns/known-typosquats.js.map +1 -0
  42. package/dist/scanners/dependency-integrity/rules/hash-validation.rule.d.ts +3 -0
  43. package/dist/scanners/dependency-integrity/rules/hash-validation.rule.d.ts.map +1 -0
  44. package/dist/scanners/dependency-integrity/rules/hash-validation.rule.js +64 -0
  45. package/dist/scanners/dependency-integrity/rules/hash-validation.rule.js.map +1 -0
  46. package/dist/scanners/dependency-integrity/rules/transitive-drift.rule.d.ts +3 -0
  47. package/dist/scanners/dependency-integrity/rules/transitive-drift.rule.d.ts.map +1 -0
  48. package/dist/scanners/dependency-integrity/rules/transitive-drift.rule.js +69 -0
  49. package/dist/scanners/dependency-integrity/rules/transitive-drift.rule.js.map +1 -0
  50. package/dist/scanners/dependency-integrity/rules/typosquat-check.rule.d.ts +3 -0
  51. package/dist/scanners/dependency-integrity/rules/typosquat-check.rule.d.ts.map +1 -0
  52. package/dist/scanners/dependency-integrity/rules/typosquat-check.rule.js +94 -0
  53. package/dist/scanners/dependency-integrity/rules/typosquat-check.rule.js.map +1 -0
  54. package/dist/scanners/dependency-integrity/scanner.d.ts +3 -0
  55. package/dist/scanners/dependency-integrity/scanner.d.ts.map +1 -0
  56. package/dist/scanners/dependency-integrity/scanner.js +26 -0
  57. package/dist/scanners/dependency-integrity/scanner.js.map +1 -0
  58. package/dist/scanners/runtime-monitor/index.d.ts +2 -0
  59. package/dist/scanners/runtime-monitor/index.d.ts.map +1 -0
  60. package/dist/scanners/runtime-monitor/index.js +6 -0
  61. package/dist/scanners/runtime-monitor/index.js.map +1 -0
  62. package/dist/scanners/runtime-monitor/patterns/known-bad-destinations.d.ts +8 -0
  63. package/dist/scanners/runtime-monitor/patterns/known-bad-destinations.d.ts.map +1 -0
  64. package/dist/scanners/runtime-monitor/patterns/known-bad-destinations.js +52 -0
  65. package/dist/scanners/runtime-monitor/patterns/known-bad-destinations.js.map +1 -0
  66. package/dist/scanners/runtime-monitor/rules/filesystem-access.rule.d.ts +3 -0
  67. package/dist/scanners/runtime-monitor/rules/filesystem-access.rule.d.ts.map +1 -0
  68. package/dist/scanners/runtime-monitor/rules/filesystem-access.rule.js +68 -0
  69. package/dist/scanners/runtime-monitor/rules/filesystem-access.rule.js.map +1 -0
  70. package/dist/scanners/runtime-monitor/rules/outbound-network.rule.d.ts +3 -0
  71. package/dist/scanners/runtime-monitor/rules/outbound-network.rule.d.ts.map +1 -0
  72. package/dist/scanners/runtime-monitor/rules/outbound-network.rule.js +62 -0
  73. package/dist/scanners/runtime-monitor/rules/outbound-network.rule.js.map +1 -0
  74. package/dist/scanners/runtime-monitor/rules/process-spawn.rule.d.ts +3 -0
  75. package/dist/scanners/runtime-monitor/rules/process-spawn.rule.d.ts.map +1 -0
  76. package/dist/scanners/runtime-monitor/rules/process-spawn.rule.js +55 -0
  77. package/dist/scanners/runtime-monitor/rules/process-spawn.rule.js.map +1 -0
  78. package/dist/scanners/runtime-monitor/rules/resource-anomaly.rule.d.ts +3 -0
  79. package/dist/scanners/runtime-monitor/rules/resource-anomaly.rule.d.ts.map +1 -0
  80. package/dist/scanners/runtime-monitor/rules/resource-anomaly.rule.js +76 -0
  81. package/dist/scanners/runtime-monitor/rules/resource-anomaly.rule.js.map +1 -0
  82. package/dist/scanners/runtime-monitor/scanner.d.ts +3 -0
  83. package/dist/scanners/runtime-monitor/scanner.d.ts.map +1 -0
  84. package/dist/scanners/runtime-monitor/scanner.js +28 -0
  85. package/dist/scanners/runtime-monitor/scanner.js.map +1 -0
  86. package/dist/scanners/secrets-exposure/index.d.ts +2 -0
  87. package/dist/scanners/secrets-exposure/index.d.ts.map +1 -0
  88. package/dist/scanners/secrets-exposure/index.js +6 -0
  89. package/dist/scanners/secrets-exposure/index.js.map +1 -0
  90. package/dist/scanners/secrets-exposure/patterns/secret-signatures.d.ts +7 -0
  91. package/dist/scanners/secrets-exposure/patterns/secret-signatures.d.ts.map +1 -0
  92. package/dist/scanners/secrets-exposure/patterns/secret-signatures.js +76 -0
  93. package/dist/scanners/secrets-exposure/patterns/secret-signatures.js.map +1 -0
  94. package/dist/scanners/secrets-exposure/rules/entropy-check.rule.d.ts +3 -0
  95. package/dist/scanners/secrets-exposure/rules/entropy-check.rule.d.ts.map +1 -0
  96. package/dist/scanners/secrets-exposure/rules/entropy-check.rule.js +77 -0
  97. package/dist/scanners/secrets-exposure/rules/entropy-check.rule.js.map +1 -0
  98. package/dist/scanners/secrets-exposure/rules/known-patterns.rule.d.ts +3 -0
  99. package/dist/scanners/secrets-exposure/rules/known-patterns.rule.d.ts.map +1 -0
  100. package/dist/scanners/secrets-exposure/rules/known-patterns.rule.js +62 -0
  101. package/dist/scanners/secrets-exposure/rules/known-patterns.rule.js.map +1 -0
  102. package/dist/scanners/secrets-exposure/rules/response-echo.rule.d.ts +3 -0
  103. package/dist/scanners/secrets-exposure/rules/response-echo.rule.d.ts.map +1 -0
  104. package/dist/scanners/secrets-exposure/rules/response-echo.rule.js +67 -0
  105. package/dist/scanners/secrets-exposure/rules/response-echo.rule.js.map +1 -0
  106. package/dist/scanners/secrets-exposure/scanner.d.ts +3 -0
  107. package/dist/scanners/secrets-exposure/scanner.d.ts.map +1 -0
  108. package/dist/scanners/secrets-exposure/scanner.js +26 -0
  109. package/dist/scanners/secrets-exposure/scanner.js.map +1 -0
  110. package/dist/types/config.types.d.ts +22 -0
  111. package/dist/types/config.types.d.ts.map +1 -0
  112. package/dist/types/config.types.js +15 -0
  113. package/dist/types/config.types.js.map +1 -0
  114. package/dist/types/context.types.d.ts +23 -0
  115. package/dist/types/context.types.d.ts.map +1 -0
  116. package/dist/types/context.types.js +3 -0
  117. package/dist/types/context.types.js.map +1 -0
  118. package/dist/types/finding.types.d.ts +33 -0
  119. package/dist/types/finding.types.d.ts.map +1 -0
  120. package/dist/types/finding.types.js +3 -0
  121. package/dist/types/finding.types.js.map +1 -0
  122. package/dist/types/index.d.ts +5 -0
  123. package/dist/types/index.d.ts.map +1 -0
  124. package/dist/types/index.js +6 -0
  125. package/dist/types/index.js.map +1 -0
  126. package/package.json +32 -0
  127. package/src/engine.ts +129 -0
  128. package/src/index.ts +28 -0
  129. package/src/scanners/ai-tool-integrity/index.ts +1 -0
  130. package/src/scanners/ai-tool-integrity/patterns/injection-signatures.ts +53 -0
  131. package/src/scanners/ai-tool-integrity/rules/prompt-injection.rule.ts +50 -0
  132. package/src/scanners/ai-tool-integrity/rules/tool-manifest-drift.rule.ts +81 -0
  133. package/src/scanners/ai-tool-integrity/rules/unauthorized-model-call.rule.ts +59 -0
  134. package/src/scanners/ai-tool-integrity/scanner.ts +25 -0
  135. package/src/scanners/dependency-integrity/index.ts +1 -0
  136. package/src/scanners/dependency-integrity/patterns/known-typosquats.ts +41 -0
  137. package/src/scanners/dependency-integrity/rules/hash-validation.rule.ts +72 -0
  138. package/src/scanners/dependency-integrity/rules/transitive-drift.rule.ts +71 -0
  139. package/src/scanners/dependency-integrity/rules/typosquat-check.rule.ts +100 -0
  140. package/src/scanners/dependency-integrity/scanner.ts +25 -0
  141. package/src/scanners/runtime-monitor/index.ts +1 -0
  142. package/src/scanners/runtime-monitor/patterns/known-bad-destinations.ts +55 -0
  143. package/src/scanners/runtime-monitor/rules/filesystem-access.rule.ts +74 -0
  144. package/src/scanners/runtime-monitor/rules/outbound-network.rule.ts +67 -0
  145. package/src/scanners/runtime-monitor/rules/process-spawn.rule.ts +58 -0
  146. package/src/scanners/runtime-monitor/rules/resource-anomaly.rule.ts +79 -0
  147. package/src/scanners/runtime-monitor/scanner.ts +27 -0
  148. package/src/scanners/secrets-exposure/index.ts +1 -0
  149. package/src/scanners/secrets-exposure/patterns/secret-signatures.ts +78 -0
  150. package/src/scanners/secrets-exposure/rules/entropy-check.rule.ts +79 -0
  151. package/src/scanners/secrets-exposure/rules/known-patterns.rule.ts +64 -0
  152. package/src/scanners/secrets-exposure/rules/response-echo.rule.ts +70 -0
  153. package/src/scanners/secrets-exposure/scanner.ts +25 -0
  154. package/src/types/config.types.ts +40 -0
  155. package/src/types/context.types.ts +25 -0
  156. package/src/types/finding.types.ts +36 -0
  157. package/src/types/index.ts +21 -0
  158. package/tsconfig.json +21 -0
  159. package/watchman.config.ts +16 -0
@@ -0,0 +1,72 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import type { Rule, ScanContext } from '../../../types/index.js';
4
+
5
+ interface PackageLockDep {
6
+ version?: string;
7
+ resolved?: string;
8
+ integrity?: string;
9
+ [key: string]: unknown;
10
+ }
11
+
12
+ export const hashValidationRule: Rule = {
13
+ id: 'hash-validation',
14
+ name: 'Lockfile Hash Validation',
15
+ description: 'Verifies that lockfile entries have integrity hashes and use secure algorithms',
16
+
17
+ async run(ctx: ScanContext): Promise<void> {
18
+ const lockfiles = ctx.files.filter((f) => basename(f) === 'package-lock.json');
19
+
20
+ for (const file of lockfiles) {
21
+ let content: string;
22
+ try {
23
+ content = await readFile(file, 'utf-8');
24
+ } catch {
25
+ continue;
26
+ }
27
+
28
+ let parsed: { packages?: Record<string, PackageLockDep> };
29
+ try {
30
+ parsed = JSON.parse(content);
31
+ } catch {
32
+ ctx.addFinding({
33
+ rule: 'hash-validation',
34
+ severity: 'high',
35
+ message: 'Lockfile is not valid JSON — may have been tampered with',
36
+ location: { file },
37
+ });
38
+ continue;
39
+ }
40
+
41
+ const packages = parsed.packages;
42
+ if (!packages) continue;
43
+
44
+ for (const [name, dep] of Object.entries(packages)) {
45
+ if (!name || name === '') continue; // root package
46
+
47
+ if (!dep.integrity) {
48
+ ctx.addFinding({
49
+ rule: 'hash-validation',
50
+ severity: 'high',
51
+ message: `Package "${name}" has no integrity hash in lockfile`,
52
+ location: { file },
53
+ suggestion: 'Run npm install to regenerate the lockfile with integrity hashes.',
54
+ metadata: { package: name, version: dep.version },
55
+ });
56
+ continue;
57
+ }
58
+
59
+ if (dep.integrity.startsWith('sha1-')) {
60
+ ctx.addFinding({
61
+ rule: 'hash-validation',
62
+ severity: 'medium',
63
+ message: `Package "${name}" uses weak SHA-1 integrity hash`,
64
+ location: { file },
65
+ suggestion: 'Regenerate lockfile to use SHA-512 integrity hashes.',
66
+ metadata: { package: name, version: dep.version },
67
+ });
68
+ }
69
+ }
70
+ }
71
+ },
72
+ };
@@ -0,0 +1,71 @@
1
+ import { readFile, access } from 'node:fs/promises';
2
+ import { join, dirname, basename } from 'node:path';
3
+ import type { Rule, ScanContext } from '../../../types/index.js';
4
+
5
+ export const transitiveDriftRule: Rule = {
6
+ id: 'transitive-drift',
7
+ name: 'Transitive Dependency Drift',
8
+ description: 'Detects when package.json and lockfile are out of sync, indicating potential supply chain risk',
9
+
10
+ async run(ctx: ScanContext): Promise<void> {
11
+ const packageFiles = ctx.files.filter((f) => basename(f) === 'package.json');
12
+
13
+ for (const pkgFile of packageFiles) {
14
+ const dir = dirname(pkgFile);
15
+ const lockFile = join(dir, 'package-lock.json');
16
+
17
+ // Check if lockfile exists
18
+ try {
19
+ await access(lockFile);
20
+ } catch {
21
+ ctx.addFinding({
22
+ rule: 'transitive-drift',
23
+ severity: 'medium',
24
+ message: 'No lockfile found alongside package.json',
25
+ location: { file: pkgFile },
26
+ suggestion: 'Run npm install to generate a lockfile. Lockfiles pin transitive dependencies.',
27
+ });
28
+ continue;
29
+ }
30
+
31
+ let pkgContent: string;
32
+ let lockContent: string;
33
+ try {
34
+ pkgContent = await readFile(pkgFile, 'utf-8');
35
+ lockContent = await readFile(lockFile, 'utf-8');
36
+ } catch {
37
+ continue;
38
+ }
39
+
40
+ let pkg: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
41
+ let lock: { packages?: Record<string, { version?: string }> };
42
+ try {
43
+ pkg = JSON.parse(pkgContent);
44
+ lock = JSON.parse(lockContent);
45
+ } catch {
46
+ continue;
47
+ }
48
+
49
+ if (!lock.packages) continue;
50
+
51
+ const declared = {
52
+ ...pkg.dependencies,
53
+ ...pkg.devDependencies,
54
+ };
55
+
56
+ for (const [name, range] of Object.entries(declared)) {
57
+ const lockEntry = lock.packages[`node_modules/${name}`];
58
+ if (!lockEntry) {
59
+ ctx.addFinding({
60
+ rule: 'transitive-drift',
61
+ severity: 'high',
62
+ message: `Package "${name}" declared in package.json but missing from lockfile`,
63
+ location: { file: pkgFile },
64
+ suggestion: 'Run npm install to sync the lockfile. Drift creates supply chain risk.',
65
+ metadata: { package: name, declaredRange: range },
66
+ });
67
+ }
68
+ }
69
+ }
70
+ },
71
+ };
@@ -0,0 +1,100 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { basename } from 'node:path';
3
+ import type { Rule, ScanContext } from '../../../types/index.js';
4
+ import { KNOWN_TYPOSQUATS, SUBSTITUTION_PAIRS } from '../patterns/known-typosquats.js';
5
+
6
+ function normalize(name: string): string {
7
+ let n = name.toLowerCase();
8
+ for (const [from, to] of SUBSTITUTION_PAIRS) {
9
+ n = n.replaceAll(from, to);
10
+ }
11
+ return n;
12
+ }
13
+
14
+ function levenshtein(a: string, b: string): number {
15
+ const m = a.length;
16
+ const n = b.length;
17
+ const dp: number[][] = Array.from({ length: m + 1 }, (_, i) =>
18
+ Array.from({ length: n + 1 }, (_, j) => (i === 0 ? j : j === 0 ? i : 0)),
19
+ );
20
+ for (let i = 1; i <= m; i++) {
21
+ for (let j = 1; j <= n; j++) {
22
+ dp[i][j] = a[i - 1] === b[j - 1]
23
+ ? dp[i - 1][j - 1]
24
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
25
+ }
26
+ }
27
+ return dp[m][n];
28
+ }
29
+
30
+ // Top npm packages to check typosquat distance against
31
+ const POPULAR_PACKAGES = [
32
+ 'react', 'express', 'lodash', 'axios', 'next', 'typescript',
33
+ 'webpack', 'babel', 'eslint', 'prettier', 'jest', 'vitest',
34
+ 'mongoose', 'sequelize', 'prisma', 'stripe', 'nodemailer',
35
+ 'dotenv', 'cors', 'bcrypt', 'jsonwebtoken', 'chalk',
36
+ ];
37
+
38
+ export const typosquatCheckRule: Rule = {
39
+ id: 'typosquat-check',
40
+ name: 'Typosquat Detection',
41
+ description: 'Checks dependencies against known typosquats and fuzzy-matches popular package names',
42
+
43
+ async run(ctx: ScanContext): Promise<void> {
44
+ const packageFiles = ctx.files.filter((f) => basename(f) === 'package.json');
45
+
46
+ for (const file of packageFiles) {
47
+ let content: string;
48
+ try {
49
+ content = await readFile(file, 'utf-8');
50
+ } catch {
51
+ continue;
52
+ }
53
+
54
+ let parsed: { dependencies?: Record<string, string>; devDependencies?: Record<string, string> };
55
+ try {
56
+ parsed = JSON.parse(content);
57
+ } catch {
58
+ continue;
59
+ }
60
+
61
+ const allDeps = [
62
+ ...Object.keys(parsed.dependencies ?? {}),
63
+ ...Object.keys(parsed.devDependencies ?? {}),
64
+ ];
65
+
66
+ for (const dep of allDeps) {
67
+ // Check known typosquats
68
+ const knownMatch = KNOWN_TYPOSQUATS.find((t) => t.malicious === dep);
69
+ if (knownMatch) {
70
+ ctx.addFinding({
71
+ rule: 'typosquat-check',
72
+ severity: 'critical',
73
+ message: `Known malicious package "${dep}" (typosquat of "${knownMatch.legitimate}")`,
74
+ location: { file },
75
+ suggestion: `Replace with the legitimate package: ${knownMatch.legitimate}`,
76
+ metadata: { technique: knownMatch.technique },
77
+ });
78
+ continue;
79
+ }
80
+
81
+ // Fuzzy match against popular packages
82
+ const normalized = normalize(dep);
83
+ for (const popular of POPULAR_PACKAGES) {
84
+ if (dep === popular) continue;
85
+ const dist = levenshtein(normalized, normalize(popular));
86
+ if (dist === 1) {
87
+ ctx.addFinding({
88
+ rule: 'typosquat-check',
89
+ severity: 'high',
90
+ message: `Package "${dep}" is 1 edit away from popular package "${popular}" — possible typosquat`,
91
+ location: { file },
92
+ suggestion: `Verify this is the intended package, not a typosquat of "${popular}".`,
93
+ metadata: { distance: dist, target: popular },
94
+ });
95
+ }
96
+ }
97
+ }
98
+ }
99
+ },
100
+ };
@@ -0,0 +1,25 @@
1
+ import type { Scanner, ScanContext, Rule } from '../../types/index.js';
2
+ import { hashValidationRule } from './rules/hash-validation.rule.js';
3
+ import { typosquatCheckRule } from './rules/typosquat-check.rule.js';
4
+ import { transitiveDriftRule } from './rules/transitive-drift.rule.js';
5
+
6
+ const rules: Rule[] = [
7
+ hashValidationRule,
8
+ typosquatCheckRule,
9
+ transitiveDriftRule,
10
+ ];
11
+
12
+ export const dependencyIntegrityScanner: Scanner = {
13
+ id: 'dependency-integrity',
14
+ name: 'Dependency Integrity',
15
+ description: 'Validates dependency hashes, detects typosquatting, and flags transitive drift',
16
+ rules,
17
+
18
+ async scan(ctx: ScanContext): Promise<void> {
19
+ for (const rule of rules) {
20
+ const ruleConfig = ctx.scannerConfig.rules?.[rule.id];
21
+ if (ruleConfig?.enabled === false) continue;
22
+ await rule.run(ctx);
23
+ }
24
+ },
25
+ };
@@ -0,0 +1 @@
1
+ export { runtimeMonitorScanner } from './scanner.js';
@@ -0,0 +1,55 @@
1
+ export interface BadDestination {
2
+ pattern: RegExp;
3
+ label: string;
4
+ category: 'c2' | 'exfiltration' | 'cryptominer' | 'suspicious';
5
+ }
6
+
7
+ // Patterns for known malicious or suspicious network destinations
8
+ export const KNOWN_BAD_DESTINATIONS: BadDestination[] = [
9
+ {
10
+ pattern: /ngrok\.io/i,
11
+ label: 'ngrok-tunnel',
12
+ category: 'suspicious',
13
+ },
14
+ {
15
+ pattern: /requestbin\.com|pipedream\.net|webhook\.site|hookbin\.com/i,
16
+ label: 'request-catcher',
17
+ category: 'exfiltration',
18
+ },
19
+ {
20
+ pattern: /pastebin\.com\/api|ghostbin\.com|hastebin\.com/i,
21
+ label: 'paste-service',
22
+ category: 'exfiltration',
23
+ },
24
+ {
25
+ pattern: /coinhive\.min\.js|coin-hive|cryptonight\.wasm/i,
26
+ label: 'cryptominer',
27
+ category: 'cryptominer',
28
+ },
29
+ {
30
+ pattern: /raw\.githubusercontent\.com\/[^/]+\/[^/]+\/[^/]+\/.*\.(sh|ps1|bat|exe)/i,
31
+ label: 'github-raw-script',
32
+ category: 'suspicious',
33
+ },
34
+ {
35
+ pattern: /\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{4,5}\b/,
36
+ label: 'raw-ip-with-port',
37
+ category: 'suspicious',
38
+ },
39
+ {
40
+ pattern: /(?:transfer\.sh|file\.io|0x0\.st)\/\w+/i,
41
+ label: 'ephemeral-file-host',
42
+ category: 'exfiltration',
43
+ },
44
+ ];
45
+
46
+ // Suspicious outbound ports
47
+ export const SUSPICIOUS_PORTS = new Set([
48
+ 4444, // Metasploit default
49
+ 5555, // Common reverse shell
50
+ 6666, // IRC / backdoor
51
+ 8888, // Alternative HTTP / C2
52
+ 9001, // Tor
53
+ 9050, // Tor SOCKS
54
+ 31337, // Back Orifice
55
+ ]);
@@ -0,0 +1,74 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { Rule, ScanContext } from '../../../types/index.js';
3
+
4
+ const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
5
+
6
+ function isCodeFile(file: string): boolean {
7
+ const dot = file.lastIndexOf('.');
8
+ return dot !== -1 && CODE_EXTENSIONS.has(file.slice(dot).toLowerCase());
9
+ }
10
+
11
+ const SENSITIVE_PATH_PATTERNS = [
12
+ { pattern: /(?:\/etc\/passwd|\/etc\/shadow|\/etc\/hosts)/g, label: 'system-file' },
13
+ { pattern: /(?:\.ssh\/|\.gnupg\/|\.aws\/credentials)/g, label: 'credential-file' },
14
+ { pattern: /(?:\.env(?:\.local)?|\.env\.production)/g, label: 'env-file' },
15
+ { pattern: /(?:\/proc\/|\/sys\/)/g, label: 'proc-sys' },
16
+ ];
17
+
18
+ const FS_WITH_USER_INPUT = [
19
+ { pattern: /(?:readFile|writeFile|readdir|unlink|rmdir)\s*\(\s*(?:req\.|params\.|query\.|body\.|args\.)/g, label: 'user-input-fs' },
20
+ { pattern: /(?:createReadStream|createWriteStream)\s*\(\s*(?:req\.|params\.|query\.|body\.)/g, label: 'user-input-stream' },
21
+ { pattern: /path\.join\s*\([^)]*(?:req\.|params\.|query\.|body\.)/g, label: 'user-input-path-join' },
22
+ ];
23
+
24
+ export const filesystemAccessRule: Rule = {
25
+ id: 'filesystem-access',
26
+ name: 'Filesystem Access Detection',
27
+ description: 'Flags access to sensitive paths and user-controlled filesystem operations',
28
+
29
+ async run(ctx: ScanContext): Promise<void> {
30
+ const codeFiles = ctx.files.filter(isCodeFile);
31
+
32
+ for (const file of codeFiles) {
33
+ let content: string;
34
+ try {
35
+ content = await readFile(file, 'utf-8');
36
+ } catch {
37
+ continue;
38
+ }
39
+
40
+ const lines = content.split('\n');
41
+ for (let i = 0; i < lines.length; i++) {
42
+ const line = lines[i];
43
+
44
+ for (const { pattern, label } of SENSITIVE_PATH_PATTERNS) {
45
+ pattern.lastIndex = 0;
46
+ if (pattern.test(line)) {
47
+ ctx.addFinding({
48
+ rule: 'filesystem-access',
49
+ severity: label === 'credential-file' ? 'high' : 'medium',
50
+ message: `Access to sensitive path detected: ${label}`,
51
+ location: { file, line: i + 1 },
52
+ evidence: line.trim().slice(0, 200),
53
+ suggestion: 'Review whether this file access is necessary and properly secured.',
54
+ });
55
+ }
56
+ }
57
+
58
+ for (const { pattern, label } of FS_WITH_USER_INPUT) {
59
+ pattern.lastIndex = 0;
60
+ if (pattern.test(line)) {
61
+ ctx.addFinding({
62
+ rule: 'filesystem-access',
63
+ severity: 'critical',
64
+ message: `User input flows into filesystem operation: ${label}`,
65
+ location: { file, line: i + 1 },
66
+ evidence: line.trim().slice(0, 200),
67
+ suggestion: 'CRITICAL: Path traversal vulnerability. Validate and sanitize all user-supplied paths.',
68
+ });
69
+ }
70
+ }
71
+ }
72
+ }
73
+ },
74
+ };
@@ -0,0 +1,67 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { Rule, ScanContext } from '../../../types/index.js';
3
+ import { KNOWN_BAD_DESTINATIONS, SUSPICIOUS_PORTS } from '../patterns/known-bad-destinations.js';
4
+
5
+ const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
6
+
7
+ function isCodeFile(file: string): boolean {
8
+ const dot = file.lastIndexOf('.');
9
+ return dot !== -1 && CODE_EXTENSIONS.has(file.slice(dot).toLowerCase());
10
+ }
11
+
12
+ const PORT_PATTERN = /:\s*(\d{4,5})\b/g;
13
+
14
+ export const outboundNetworkRule: Rule = {
15
+ id: 'outbound-network',
16
+ name: 'Outbound Network Detection',
17
+ description: 'Flags connections to known malicious destinations and suspicious ports',
18
+
19
+ async run(ctx: ScanContext): Promise<void> {
20
+ const codeFiles = ctx.files.filter(isCodeFile);
21
+
22
+ for (const file of codeFiles) {
23
+ let content: string;
24
+ try {
25
+ content = await readFile(file, 'utf-8');
26
+ } catch {
27
+ continue;
28
+ }
29
+
30
+ const lines = content.split('\n');
31
+ for (let i = 0; i < lines.length; i++) {
32
+ const line = lines[i];
33
+
34
+ // Check known bad destinations
35
+ for (const dest of KNOWN_BAD_DESTINATIONS) {
36
+ if (dest.pattern.test(line)) {
37
+ ctx.addFinding({
38
+ rule: 'outbound-network',
39
+ severity: dest.category === 'c2' ? 'critical' : dest.category === 'exfiltration' ? 'high' : 'medium',
40
+ message: `Connection to suspicious destination: ${dest.label} (${dest.category})`,
41
+ location: { file, line: i + 1 },
42
+ evidence: line.trim().slice(0, 200),
43
+ suggestion: 'Review this network destination. It matches a known suspicious pattern.',
44
+ });
45
+ }
46
+ }
47
+
48
+ // Check suspicious ports
49
+ PORT_PATTERN.lastIndex = 0;
50
+ let portMatch: RegExpExecArray | null;
51
+ while ((portMatch = PORT_PATTERN.exec(line)) !== null) {
52
+ const port = parseInt(portMatch[1], 10);
53
+ if (SUSPICIOUS_PORTS.has(port)) {
54
+ ctx.addFinding({
55
+ rule: 'outbound-network',
56
+ severity: 'high',
57
+ message: `Connection to suspicious port ${port} — commonly used for ${port === 9001 || port === 9050 ? 'Tor' : 'reverse shells/C2'}`,
58
+ location: { file, line: i + 1 },
59
+ evidence: line.trim().slice(0, 200),
60
+ suggestion: 'This port is associated with malicious tools. Verify this is legitimate.',
61
+ });
62
+ }
63
+ }
64
+ }
65
+ }
66
+ },
67
+ };
@@ -0,0 +1,58 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { Rule, ScanContext } from '../../../types/index.js';
3
+
4
+ const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
5
+
6
+ function isCodeFile(file: string): boolean {
7
+ const dot = file.lastIndexOf('.');
8
+ return dot !== -1 && CODE_EXTENSIONS.has(file.slice(dot).toLowerCase());
9
+ }
10
+
11
+ const SPAWN_PATTERNS = [
12
+ { pattern: /child_process.*(?:exec|execSync|spawn|spawnSync|fork)\s*\(/g, label: 'child_process' },
13
+ { pattern: /\bexec\s*\(\s*['"`]/g, label: 'exec-string' },
14
+ { pattern: /\beval\s*\(\s*[^)]/g, label: 'eval' },
15
+ { pattern: /new\s+Function\s*\(/g, label: 'Function-constructor' },
16
+ { pattern: /child_process.*\bexec\s*\(\s*(?:req\.|params\.|query\.|body\.)/g, label: 'user-input-exec' },
17
+ { pattern: /\.exec\s*\(\s*`[^`]*\$\{/g, label: 'template-exec' },
18
+ ];
19
+
20
+ export const processSpawnRule: Rule = {
21
+ id: 'process-spawn',
22
+ name: 'Process Spawn Detection',
23
+ description: 'Flags shell command execution and dynamic code evaluation patterns',
24
+
25
+ async run(ctx: ScanContext): Promise<void> {
26
+ const codeFiles = ctx.files.filter(isCodeFile);
27
+
28
+ for (const file of codeFiles) {
29
+ let content: string;
30
+ try {
31
+ content = await readFile(file, 'utf-8');
32
+ } catch {
33
+ continue;
34
+ }
35
+
36
+ const lines = content.split('\n');
37
+ for (let i = 0; i < lines.length; i++) {
38
+ const line = lines[i];
39
+ for (const { pattern, label } of SPAWN_PATTERNS) {
40
+ pattern.lastIndex = 0;
41
+ if (pattern.test(line)) {
42
+ const isUserInput = label === 'user-input-exec' || label === 'template-exec';
43
+ ctx.addFinding({
44
+ rule: 'process-spawn',
45
+ severity: isUserInput ? 'critical' : 'medium',
46
+ message: `Process spawn/eval detected: ${label}`,
47
+ location: { file, line: i + 1 },
48
+ evidence: line.trim().slice(0, 200),
49
+ suggestion: isUserInput
50
+ ? 'CRITICAL: User input flows into shell execution. This is a command injection vulnerability.'
51
+ : 'Review whether this shell execution is necessary. Prefer native Node.js APIs.',
52
+ });
53
+ }
54
+ }
55
+ }
56
+ }
57
+ },
58
+ };
@@ -0,0 +1,79 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import type { Rule, ScanContext } from '../../../types/index.js';
3
+
4
+ const CODE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs']);
5
+
6
+ function isCodeFile(file: string): boolean {
7
+ const dot = file.lastIndexOf('.');
8
+ return dot !== -1 && CODE_EXTENSIONS.has(file.slice(dot).toLowerCase());
9
+ }
10
+
11
+ const ANOMALY_PATTERNS = [
12
+ {
13
+ pattern: /while\s*\(\s*true\s*\)/g,
14
+ label: 'infinite-loop',
15
+ severity: 'medium' as const,
16
+ message: 'Infinite loop detected — potential denial of service',
17
+ },
18
+ {
19
+ pattern: /setInterval\s*\([^,]+,\s*\d{1,3}\s*\)/g,
20
+ label: 'tight-interval',
21
+ severity: 'medium' as const,
22
+ message: 'Very short setInterval (< 1s) — potential resource exhaustion',
23
+ },
24
+ {
25
+ pattern: /Buffer\.alloc(?:Unsafe)?\s*\(\s*(?:\d{8,}|[a-zA-Z_$][\w$]*\s*\*\s*[a-zA-Z_$][\w$]*)\s*\)/g,
26
+ label: 'large-buffer',
27
+ severity: 'medium' as const,
28
+ message: 'Large buffer allocation — potential memory exhaustion attack',
29
+ },
30
+ {
31
+ pattern: /new\s+RegExp\s*\(\s*(?:req\.|params\.|query\.|body\.|args\.)/g,
32
+ label: 'user-regex',
33
+ severity: 'high' as const,
34
+ message: 'User input used in RegExp constructor — ReDoS vulnerability',
35
+ },
36
+ {
37
+ pattern: /crypto\.(?:pbkdf2|scrypt).*(?:iterations|cost)\s*[:=]\s*(?:req\.|params\.|query\.|body\.)/g,
38
+ label: 'user-controlled-crypto',
39
+ severity: 'high' as const,
40
+ message: 'User input controls crypto parameters — potential algorithmic complexity attack',
41
+ },
42
+ ];
43
+
44
+ export const resourceAnomalyRule: Rule = {
45
+ id: 'resource-anomaly',
46
+ name: 'Resource Anomaly Detection',
47
+ description: 'Detects patterns that could cause resource exhaustion or denial of service',
48
+
49
+ async run(ctx: ScanContext): Promise<void> {
50
+ const codeFiles = ctx.files.filter(isCodeFile);
51
+
52
+ for (const file of codeFiles) {
53
+ let content: string;
54
+ try {
55
+ content = await readFile(file, 'utf-8');
56
+ } catch {
57
+ continue;
58
+ }
59
+
60
+ const lines = content.split('\n');
61
+ for (let i = 0; i < lines.length; i++) {
62
+ const line = lines[i];
63
+ for (const anomaly of ANOMALY_PATTERNS) {
64
+ anomaly.pattern.lastIndex = 0;
65
+ if (anomaly.pattern.test(line)) {
66
+ ctx.addFinding({
67
+ rule: 'resource-anomaly',
68
+ severity: anomaly.severity,
69
+ message: anomaly.message,
70
+ location: { file, line: i + 1 },
71
+ evidence: line.trim().slice(0, 200),
72
+ suggestion: 'Review this pattern for potential resource exhaustion.',
73
+ });
74
+ }
75
+ }
76
+ }
77
+ }
78
+ },
79
+ };
@@ -0,0 +1,27 @@
1
+ import type { Scanner, ScanContext, Rule } from '../../types/index.js';
2
+ import { processSpawnRule } from './rules/process-spawn.rule.js';
3
+ import { filesystemAccessRule } from './rules/filesystem-access.rule.js';
4
+ import { outboundNetworkRule } from './rules/outbound-network.rule.js';
5
+ import { resourceAnomalyRule } from './rules/resource-anomaly.rule.js';
6
+
7
+ const rules: Rule[] = [
8
+ processSpawnRule,
9
+ filesystemAccessRule,
10
+ outboundNetworkRule,
11
+ resourceAnomalyRule,
12
+ ];
13
+
14
+ export const runtimeMonitorScanner: Scanner = {
15
+ id: 'runtime-monitor',
16
+ name: 'Runtime Monitor',
17
+ description: 'Detects suspicious runtime behavior: process spawning, filesystem access, network calls, resource anomalies',
18
+ rules,
19
+
20
+ async scan(ctx: ScanContext): Promise<void> {
21
+ for (const rule of rules) {
22
+ const ruleConfig = ctx.scannerConfig.rules?.[rule.id];
23
+ if (ruleConfig?.enabled === false) continue;
24
+ await rule.run(ctx);
25
+ }
26
+ },
27
+ };
@@ -0,0 +1 @@
1
+ export { secretsExposureScanner } from './scanner.js';