mitnick-cli 1.0.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 (131) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +193 -0
  3. package/dist/analyzers/analyzer.interface.d.ts +32 -0
  4. package/dist/analyzers/analyzer.interface.d.ts.map +1 -0
  5. package/dist/analyzers/analyzer.interface.js +2 -0
  6. package/dist/analyzers/analyzer.interface.js.map +1 -0
  7. package/dist/analyzers/analyzer.registry.d.ts +16 -0
  8. package/dist/analyzers/analyzer.registry.d.ts.map +1 -0
  9. package/dist/analyzers/analyzer.registry.js +40 -0
  10. package/dist/analyzers/analyzer.registry.js.map +1 -0
  11. package/dist/analyzers/dependency-confusion/index.d.ts +14 -0
  12. package/dist/analyzers/dependency-confusion/index.d.ts.map +1 -0
  13. package/dist/analyzers/dependency-confusion/index.js +147 -0
  14. package/dist/analyzers/dependency-confusion/index.js.map +1 -0
  15. package/dist/analyzers/dormant-package/index.d.ts +14 -0
  16. package/dist/analyzers/dormant-package/index.d.ts.map +1 -0
  17. package/dist/analyzers/dormant-package/index.js +137 -0
  18. package/dist/analyzers/dormant-package/index.js.map +1 -0
  19. package/dist/analyzers/file-based-analyzer.d.ts +20 -0
  20. package/dist/analyzers/file-based-analyzer.d.ts.map +1 -0
  21. package/dist/analyzers/file-based-analyzer.js +35 -0
  22. package/dist/analyzers/file-based-analyzer.js.map +1 -0
  23. package/dist/analyzers/install-scripts/index.d.ts +13 -0
  24. package/dist/analyzers/install-scripts/index.d.ts.map +1 -0
  25. package/dist/analyzers/install-scripts/index.js +125 -0
  26. package/dist/analyzers/install-scripts/index.js.map +1 -0
  27. package/dist/analyzers/license/index.d.ts +12 -0
  28. package/dist/analyzers/license/index.d.ts.map +1 -0
  29. package/dist/analyzers/license/index.js +199 -0
  30. package/dist/analyzers/license/index.js.map +1 -0
  31. package/dist/analyzers/maintainer/index.d.ts +12 -0
  32. package/dist/analyzers/maintainer/index.d.ts.map +1 -0
  33. package/dist/analyzers/maintainer/index.js +93 -0
  34. package/dist/analyzers/maintainer/index.js.map +1 -0
  35. package/dist/analyzers/network-calls/index.d.ts +15 -0
  36. package/dist/analyzers/network-calls/index.d.ts.map +1 -0
  37. package/dist/analyzers/network-calls/index.js +212 -0
  38. package/dist/analyzers/network-calls/index.js.map +1 -0
  39. package/dist/analyzers/obfuscation/index.d.ts +19 -0
  40. package/dist/analyzers/obfuscation/index.d.ts.map +1 -0
  41. package/dist/analyzers/obfuscation/index.js +218 -0
  42. package/dist/analyzers/obfuscation/index.js.map +1 -0
  43. package/dist/analyzers/prototype-pollution/index.d.ts +18 -0
  44. package/dist/analyzers/prototype-pollution/index.d.ts.map +1 -0
  45. package/dist/analyzers/prototype-pollution/index.js +257 -0
  46. package/dist/analyzers/prototype-pollution/index.js.map +1 -0
  47. package/dist/analyzers/sensitive-data/index.d.ts +16 -0
  48. package/dist/analyzers/sensitive-data/index.d.ts.map +1 -0
  49. package/dist/analyzers/sensitive-data/index.js +254 -0
  50. package/dist/analyzers/sensitive-data/index.js.map +1 -0
  51. package/dist/analyzers/typosquatting/index.d.ts +14 -0
  52. package/dist/analyzers/typosquatting/index.d.ts.map +1 -0
  53. package/dist/analyzers/typosquatting/index.js +127 -0
  54. package/dist/analyzers/typosquatting/index.js.map +1 -0
  55. package/dist/analyzers/typosquatting/popular-packages.d.ts +9 -0
  56. package/dist/analyzers/typosquatting/popular-packages.d.ts.map +1 -0
  57. package/dist/analyzers/typosquatting/popular-packages.js +236 -0
  58. package/dist/analyzers/typosquatting/popular-packages.js.map +1 -0
  59. package/dist/analyzers/vulnerability/index.d.ts +12 -0
  60. package/dist/analyzers/vulnerability/index.d.ts.map +1 -0
  61. package/dist/analyzers/vulnerability/index.js +147 -0
  62. package/dist/analyzers/vulnerability/index.js.map +1 -0
  63. package/dist/cli/commands/check.d.ts +21 -0
  64. package/dist/cli/commands/check.d.ts.map +1 -0
  65. package/dist/cli/commands/check.js +204 -0
  66. package/dist/cli/commands/check.js.map +1 -0
  67. package/dist/cli/formatters/formatter.interface.d.ts +14 -0
  68. package/dist/cli/formatters/formatter.interface.d.ts.map +1 -0
  69. package/dist/cli/formatters/formatter.interface.js +2 -0
  70. package/dist/cli/formatters/formatter.interface.js.map +1 -0
  71. package/dist/cli/formatters/json.d.ts +12 -0
  72. package/dist/cli/formatters/json.d.ts.map +1 -0
  73. package/dist/cli/formatters/json.js +12 -0
  74. package/dist/cli/formatters/json.js.map +1 -0
  75. package/dist/cli/formatters/sarif.d.ts +13 -0
  76. package/dist/cli/formatters/sarif.d.ts.map +1 -0
  77. package/dist/cli/formatters/sarif.js +101 -0
  78. package/dist/cli/formatters/sarif.js.map +1 -0
  79. package/dist/cli/formatters/terminal.d.ts +13 -0
  80. package/dist/cli/formatters/terminal.d.ts.map +1 -0
  81. package/dist/cli/formatters/terminal.js +110 -0
  82. package/dist/cli/formatters/terminal.js.map +1 -0
  83. package/dist/cli/index.d.ts +9 -0
  84. package/dist/cli/index.d.ts.map +1 -0
  85. package/dist/cli/index.js +86 -0
  86. package/dist/cli/index.js.map +1 -0
  87. package/dist/core/engine.d.ts +23 -0
  88. package/dist/core/engine.d.ts.map +1 -0
  89. package/dist/core/engine.js +55 -0
  90. package/dist/core/engine.js.map +1 -0
  91. package/dist/core/scorer.d.ts +30 -0
  92. package/dist/core/scorer.d.ts.map +1 -0
  93. package/dist/core/scorer.js +88 -0
  94. package/dist/core/scorer.js.map +1 -0
  95. package/dist/core/types.d.ts +76 -0
  96. package/dist/core/types.d.ts.map +1 -0
  97. package/dist/core/types.js +30 -0
  98. package/dist/core/types.js.map +1 -0
  99. package/dist/index.d.ts +33 -0
  100. package/dist/index.d.ts.map +1 -0
  101. package/dist/index.js +30 -0
  102. package/dist/index.js.map +1 -0
  103. package/dist/registry/client.d.ts +27 -0
  104. package/dist/registry/client.d.ts.map +1 -0
  105. package/dist/registry/client.js +189 -0
  106. package/dist/registry/client.js.map +1 -0
  107. package/dist/registry/tarball.d.ts +34 -0
  108. package/dist/registry/tarball.d.ts.map +1 -0
  109. package/dist/registry/tarball.js +103 -0
  110. package/dist/registry/tarball.js.map +1 -0
  111. package/dist/utils/ast.d.ts +74 -0
  112. package/dist/utils/ast.d.ts.map +1 -0
  113. package/dist/utils/ast.js +150 -0
  114. package/dist/utils/ast.js.map +1 -0
  115. package/dist/utils/fs.d.ts +28 -0
  116. package/dist/utils/fs.d.ts.map +1 -0
  117. package/dist/utils/fs.js +78 -0
  118. package/dist/utils/fs.js.map +1 -0
  119. package/dist/utils/http.d.ts +40 -0
  120. package/dist/utils/http.d.ts.map +1 -0
  121. package/dist/utils/http.js +116 -0
  122. package/dist/utils/http.js.map +1 -0
  123. package/dist/utils/logger.d.ts +46 -0
  124. package/dist/utils/logger.d.ts.map +1 -0
  125. package/dist/utils/logger.js +91 -0
  126. package/dist/utils/logger.js.map +1 -0
  127. package/dist/utils/strings.d.ts +8 -0
  128. package/dist/utils/strings.d.ts.map +1 -0
  129. package/dist/utils/strings.js +12 -0
  130. package/dist/utils/strings.js.map +1 -0
  131. package/package.json +96 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mitnick Contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # mitnick
2
+
3
+ Pre-install security analysis CLI for npm packages. Analyze packages **before** installation to detect vulnerabilities, malicious code, typosquatting, and supply chain attacks.
4
+
5
+ Named after [Kevin Mitnick](https://en.wikipedia.org/wiki/Kevin_Mitnick), one of the most famous security experts in history.
6
+
7
+ ## Why?
8
+
9
+ npm supply chain attacks are escalating. In 2025 alone, packages like `debug` and `chalk` (2.6B+ weekly downloads) were compromised. Existing tools like `npm audit` only work **after** installation — by then, malicious `postinstall` scripts have already executed.
10
+
11
+ **mitnick** fetches and analyzes package tarballs from the npm registry without ever executing their code. Nothing runs on your machine except mitnick itself.
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ npm install -g mitnick
17
+ ```
18
+
19
+ Or use directly with npx:
20
+
21
+ ```bash
22
+ npx mitnick check express
23
+ ```
24
+
25
+ ## Usage
26
+
27
+ ```bash
28
+ # Check a single package
29
+ mitnick check express
30
+
31
+ # Check a specific version
32
+ mitnick check express@4.19.2
33
+
34
+ # Check multiple packages at once
35
+ mitnick check express lodash chalk
36
+
37
+ # JSON output for scripts and tooling
38
+ mitnick check --json express
39
+
40
+ # SARIF output for GitHub Security tab
41
+ mitnick check --sarif express
42
+
43
+ # CI mode — exit code 1 if any finding meets the severity threshold
44
+ mitnick check --fail-on high express
45
+
46
+ # Verbose output with extra details
47
+ mitnick check --verbose express
48
+ ```
49
+
50
+ ## Output
51
+
52
+ ```
53
+ mitnick v1.0.0 — Security Analysis
54
+
55
+ Checking express@4.19.2...
56
+
57
+ ✓ Vulnerability Scanner 2 findings
58
+ ✓ Install Scripts 0 findings
59
+ ✓ Typosquatting 0 findings
60
+ ✓ Obfuscation 0 findings
61
+ ✓ Network Calls 1 finding
62
+ ✓ Sensitive Data 0 findings
63
+ ✓ License 0 findings
64
+ ✓ Maintainer 1 finding
65
+ ✓ Dependency Confusion 0 findings
66
+ ✓ Dormant Package 0 findings
67
+ ✓ Prototype Pollution 0 findings
68
+
69
+ Score: 79/100 (C)
70
+
71
+ ┌──────────┬──────────┬──────────────────────────────────────┐
72
+ │ Severity │ Analyzer │ Finding │
73
+ ├──────────┼──────────┼──────────────────────────────────────┤
74
+ │ HIGH │ Vuln │ CVE-2024-XXXX in qs@6.5.2 │
75
+ │ MEDIUM │ Vuln │ CVE-2024-YYYY in path-to-regexp@0.1 │
76
+ │ MEDIUM │ Network │ Uses http module for outbound calls │
77
+ │ LOW │ Maint │ Single maintainer (bus factor = 1) │
78
+ └──────────┴──────────┴──────────────────────────────────────┘
79
+
80
+ Analyzed in 1.2s
81
+ ```
82
+
83
+ ## Security Analyzers
84
+
85
+ mitnick runs 11 security analyzers on every package:
86
+
87
+ | Analyzer | What it detects |
88
+ |----------|----------------|
89
+ | **Vulnerability Scanner** | Known CVEs via the [OSV](https://osv.dev) database and GitHub Advisory DB |
90
+ | **Install Scripts** | `preinstall`/`postinstall` hooks with suspicious commands (curl, wget, eval, shell spawning) |
91
+ | **Typosquatting** | Package names suspiciously similar to popular packages (Levenshtein distance, character substitution) |
92
+ | **Obfuscation** | High-entropy strings, `eval()`, `new Function()`, Base64 blobs, hex-encoded code |
93
+ | **Network Calls** | `fetch()`, `http.request()`, axios/got imports, hardcoded IP addresses |
94
+ | **Sensitive Data** | `process.env` harvesting, access to `~/.ssh`, `~/.aws`, `~/.npmrc`, credential files |
95
+ | **License** | Missing licenses, copyleft (GPL/AGPL), SPDX compliance |
96
+ | **Maintainer** | Single-maintainer risk (bus factor), new/inactive accounts |
97
+ | **Dependency Confusion** | Public packages mimicking internal/private naming patterns |
98
+ | **Dormant Package** | Packages reactivated after long inactivity (common attack vector) |
99
+ | **Prototype Pollution** | `__proto__` access, `Object.prototype` mutation, unsafe merge functions |
100
+
101
+ ## Scoring
102
+
103
+ Each package gets a score from 0 to 100 based on findings:
104
+
105
+ | Severity | Points deducted |
106
+ |----------|----------------|
107
+ | Critical | -25 |
108
+ | High | -15 |
109
+ | Medium | -8 |
110
+ | Low | -3 |
111
+ | Info | 0 |
112
+
113
+ | Score | Grade |
114
+ |-------|-------|
115
+ | 90-100 | A |
116
+ | 80-89 | B |
117
+ | 70-79 | C |
118
+ | 50-69 | D |
119
+ | 0-49 | F |
120
+
121
+ ## CI/CD Integration
122
+
123
+ ### Exit codes
124
+
125
+ Use `--fail-on` to fail your pipeline when findings meet a severity threshold:
126
+
127
+ ```bash
128
+ # Fail if any critical or high severity finding exists
129
+ mitnick check --fail-on high express
130
+ ```
131
+
132
+ Exit code `1` means findings were found at or above the threshold. Exit code `0` means the package passed.
133
+
134
+ ### GitHub Actions
135
+
136
+ ```yaml
137
+ - name: Security check dependencies
138
+ run: npx mitnick check --fail-on medium $(cat package.json | jq -r '.dependencies | keys[]')
139
+ ```
140
+
141
+ ### SARIF upload to GitHub Security tab
142
+
143
+ ```yaml
144
+ - name: Run mitnick
145
+ run: npx mitnick check --sarif express > results.sarif
146
+
147
+ - name: Upload SARIF
148
+ uses: github/codeql-action/upload-sarif@v3
149
+ with:
150
+ sarif_file: results.sarif
151
+ ```
152
+
153
+ ## Requirements
154
+
155
+ - Node.js >= 18.0.0
156
+
157
+ ## Development
158
+
159
+ ```bash
160
+ # Clone and install
161
+ git clone https://github.com/your-username/mitnick.git
162
+ cd mitnick
163
+ npm install
164
+
165
+ # Build
166
+ npm run build
167
+
168
+ # Run tests (298 tests)
169
+ npm test
170
+
171
+ # Run tests with coverage
172
+ npm run test:coverage
173
+
174
+ # Type check
175
+ npm run typecheck
176
+ ```
177
+
178
+ ## Architecture
179
+
180
+ ```
181
+ src/
182
+ ├── cli/ CLI entry point, commands, formatters (terminal/JSON/SARIF)
183
+ ├── core/ Analysis engine, scoring system, shared types
184
+ ├── registry/ npm registry client, tarball download and extraction
185
+ ├── analyzers/ 11 security analyzers (each implements Analyzer interface)
186
+ └── utils/ AST parsing, HTTP client, filesystem helpers, logger
187
+ ```
188
+
189
+ All analyzers implement a shared `Analyzer` interface and are executed in parallel. Adding a new analyzer requires zero changes to existing code (Open/Closed Principle).
190
+
191
+ ## License
192
+
193
+ [MIT](LICENSE)
@@ -0,0 +1,32 @@
1
+ import type { AnalysisContext, AnalyzerResult } from '../core/types.js';
2
+ /**
3
+ * Contract for all security analyzers.
4
+ *
5
+ * Each analyzer is a self-contained strategy that examines a specific
6
+ * security concern. Analyzers are registered with the engine and executed
7
+ * in parallel during analysis.
8
+ *
9
+ * To add a new analyzer:
10
+ * 1. Create a new directory under src/analyzers/
11
+ * 2. Implement this interface
12
+ * 3. Register it in analyzer.registry.ts
13
+ *
14
+ * No existing code needs to change (Open/Closed Principle).
15
+ */
16
+ export interface Analyzer {
17
+ /** Unique identifier for this analyzer (e.g., "vulnerability-scanner") */
18
+ readonly name: string;
19
+ /** Human-readable description shown in reports */
20
+ readonly description: string;
21
+ /**
22
+ * Analyze a package and return findings.
23
+ *
24
+ * Implementations must:
25
+ * - Never throw — catch errors and return empty findings with a warning
26
+ * - Never execute code from the analyzed package
27
+ * - Be stateless — no side effects between calls
28
+ * - Return duration in milliseconds
29
+ */
30
+ analyze(context: AnalysisContext): Promise<AnalyzerResult>;
31
+ }
32
+ //# sourceMappingURL=analyzer.interface.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer.interface.d.ts","sourceRoot":"","sources":["../../src/analyzers/analyzer.interface.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAExE;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,QAAQ;IACvB,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,kDAAkD;IAClD,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAE7B;;;;;;;;OAQG;IACH,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;CAC5D"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=analyzer.interface.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer.interface.js","sourceRoot":"","sources":["../../src/analyzers/analyzer.interface.ts"],"names":[],"mappings":""}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Analyzer registry — factory that creates all analyzer instances.
3
+ *
4
+ * Each analyzer is imported from its own directory and instantiated
5
+ * with a no-arg constructor. Adding a new analyzer requires only
6
+ * adding an import and appending to the array (Open/Closed Principle).
7
+ */
8
+ import type { Analyzer } from './analyzer.interface.js';
9
+ /**
10
+ * Create and return all registered analyzer instances.
11
+ *
12
+ * Returns a readonly array to prevent mutation of the registry
13
+ * after creation. The engine receives this array via DI.
14
+ */
15
+ export declare function createAnalyzers(): readonly Analyzer[];
16
+ //# sourceMappingURL=analyzer.registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer.registry.d.ts","sourceRoot":"","sources":["../../src/analyzers/analyzer.registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAC;AAaxD;;;;;GAKG;AACH,wBAAgB,eAAe,IAAI,SAAS,QAAQ,EAAE,CAcrD"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Analyzer registry — factory that creates all analyzer instances.
3
+ *
4
+ * Each analyzer is imported from its own directory and instantiated
5
+ * with a no-arg constructor. Adding a new analyzer requires only
6
+ * adding an import and appending to the array (Open/Closed Principle).
7
+ */
8
+ import { VulnerabilityAnalyzer } from './vulnerability/index.js';
9
+ import { InstallScriptAnalyzer } from './install-scripts/index.js';
10
+ import { TyposquattingAnalyzer } from './typosquatting/index.js';
11
+ import { ObfuscationAnalyzer } from './obfuscation/index.js';
12
+ import { NetworkCallsAnalyzer } from './network-calls/index.js';
13
+ import { SensitiveDataAnalyzer } from './sensitive-data/index.js';
14
+ import { LicenseAnalyzer } from './license/index.js';
15
+ import { MaintainerAnalyzer } from './maintainer/index.js';
16
+ import { DependencyConfusionAnalyzer } from './dependency-confusion/index.js';
17
+ import { DormantPackageAnalyzer } from './dormant-package/index.js';
18
+ import { PrototypePollutionAnalyzer } from './prototype-pollution/index.js';
19
+ /**
20
+ * Create and return all registered analyzer instances.
21
+ *
22
+ * Returns a readonly array to prevent mutation of the registry
23
+ * after creation. The engine receives this array via DI.
24
+ */
25
+ export function createAnalyzers() {
26
+ return [
27
+ new VulnerabilityAnalyzer(),
28
+ new InstallScriptAnalyzer(),
29
+ new TyposquattingAnalyzer(),
30
+ new ObfuscationAnalyzer(),
31
+ new NetworkCallsAnalyzer(),
32
+ new SensitiveDataAnalyzer(),
33
+ new LicenseAnalyzer(),
34
+ new MaintainerAnalyzer(),
35
+ new DependencyConfusionAnalyzer(),
36
+ new DormantPackageAnalyzer(),
37
+ new PrototypePollutionAnalyzer(),
38
+ ];
39
+ }
40
+ //# sourceMappingURL=analyzer.registry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"analyzer.registry.js","sourceRoot":"","sources":["../../src/analyzers/analyzer.registry.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,qBAAqB,EAAE,MAAM,4BAA4B,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAC7D,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAC3D,OAAO,EAAE,2BAA2B,EAAE,MAAM,iCAAiC,CAAC;AAC9E,OAAO,EAAE,sBAAsB,EAAE,MAAM,4BAA4B,CAAC;AACpE,OAAO,EAAE,0BAA0B,EAAE,MAAM,gCAAgC,CAAC;AAE5E;;;;;GAKG;AACH,MAAM,UAAU,eAAe;IAC7B,OAAO;QACL,IAAI,qBAAqB,EAAE;QAC3B,IAAI,qBAAqB,EAAE;QAC3B,IAAI,qBAAqB,EAAE;QAC3B,IAAI,mBAAmB,EAAE;QACzB,IAAI,oBAAoB,EAAE;QAC1B,IAAI,qBAAqB,EAAE;QAC3B,IAAI,eAAe,EAAE;QACrB,IAAI,kBAAkB,EAAE;QACxB,IAAI,2BAA2B,EAAE;QACjC,IAAI,sBAAsB,EAAE;QAC5B,IAAI,0BAA0B,EAAE;KACxB,CAAC;AACb,CAAC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Dependency Confusion Analyzer — detects packages that may be exploiting
3
+ * dependency confusion by mimicking internal/private package naming patterns.
4
+ */
5
+ import type { Analyzer } from '../analyzer.interface.js';
6
+ import type { AnalysisContext, AnalyzerResult } from '../../core/types.js';
7
+ export declare class DependencyConfusionAnalyzer implements Analyzer {
8
+ readonly name = "dependency-confusion";
9
+ readonly description = "Detects packages that may exploit dependency confusion attacks";
10
+ analyze(context: AnalysisContext): Promise<AnalyzerResult>;
11
+ private isRecentlyPublished;
12
+ private hasOrgLookingSegments;
13
+ }
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/analyzers/dependency-confusion/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAW,MAAM,qBAAqB,CAAC;AAkCpF,qBAAa,2BAA4B,YAAW,QAAQ;IAC1D,QAAQ,CAAC,IAAI,0BAA0B;IACvC,QAAQ,CAAC,WAAW,oEAAoE;IAExF,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC;IAiG1D,OAAO,CAAC,mBAAmB;IAe3B,OAAO,CAAC,qBAAqB;CAsB9B"}
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Dependency Confusion Analyzer — detects packages that may be exploiting
3
+ * dependency confusion by mimicking internal/private package naming patterns.
4
+ */
5
+ import { logger } from '../../utils/logger.js';
6
+ // ─── Constants ────────────────────────────────────────────
7
+ /** Suffixes commonly used for internal packages. */
8
+ const INTERNAL_SUFFIXES = [
9
+ '-internal',
10
+ '-private',
11
+ '-corp',
12
+ '-enterprise',
13
+ '-dev',
14
+ '-staging',
15
+ '-infra',
16
+ '-platform',
17
+ '-core-internal',
18
+ '-sdk-internal',
19
+ ];
20
+ /** Prefixes commonly used for company/org internal packages. */
21
+ const ORG_PREFIXES = [
22
+ 'company-',
23
+ 'corp-',
24
+ 'internal-',
25
+ 'private-',
26
+ 'enterprise-',
27
+ 'intranet-',
28
+ ];
29
+ /** Maximum age in days for a "very recently published" package. */
30
+ const RECENT_PUBLISH_DAYS = 30;
31
+ // ─── Analyzer ─────────────────────────────────────────────
32
+ export class DependencyConfusionAnalyzer {
33
+ name = 'dependency-confusion';
34
+ description = 'Detects packages that may exploit dependency confusion attacks';
35
+ analyze(context) {
36
+ const start = performance.now();
37
+ const findings = [];
38
+ try {
39
+ const packageName = context.packageName;
40
+ // Skip scoped packages — they're namespaced and less vulnerable to confusion
41
+ if (packageName.startsWith('@')) {
42
+ return Promise.resolve({
43
+ analyzer: this.name,
44
+ findings: [],
45
+ duration: performance.now() - start,
46
+ });
47
+ }
48
+ const lowerName = packageName.toLowerCase();
49
+ // Check for internal-looking suffixes
50
+ const matchedSuffix = INTERNAL_SUFFIXES.find((suffix) => lowerName.endsWith(suffix));
51
+ if (matchedSuffix !== undefined) {
52
+ findings.push({
53
+ analyzer: this.name,
54
+ severity: 'high',
55
+ title: `Package name suggests internal origin: "${matchedSuffix}" suffix`,
56
+ description: `The package name "${packageName}" ends with "${matchedSuffix}", ` +
57
+ 'which is commonly used for internal/private packages. ' +
58
+ 'This public package may be attempting a dependency confusion attack.',
59
+ recommendation: 'Verify this is the intended package and not a malicious squatter ' +
60
+ 'targeting your internal package name.',
61
+ });
62
+ }
63
+ // Check for org-like prefixes
64
+ const matchedPrefix = ORG_PREFIXES.find((prefix) => lowerName.startsWith(prefix));
65
+ if (matchedPrefix !== undefined) {
66
+ findings.push({
67
+ analyzer: this.name,
68
+ severity: 'high',
69
+ title: `Package name suggests organizational origin: "${matchedPrefix}" prefix`,
70
+ description: `The package name "${packageName}" starts with "${matchedPrefix}", ` +
71
+ 'which mimics organizational/internal naming conventions. ' +
72
+ 'A public package with this naming pattern may be a dependency confusion attempt.',
73
+ recommendation: 'Confirm this package is from a trusted source, not mimicking an internal dependency.',
74
+ });
75
+ }
76
+ // Check for very recently published packages with organizational naming
77
+ const isRecentlyPublished = this.isRecentlyPublished(context);
78
+ const hasOrgNaming = matchedSuffix !== undefined || matchedPrefix !== undefined;
79
+ if (isRecentlyPublished && hasOrgNaming) {
80
+ findings.push({
81
+ analyzer: this.name,
82
+ severity: 'high',
83
+ title: 'Recently published package with internal naming pattern',
84
+ description: `The package "${packageName}" was recently published and uses naming conventions ` +
85
+ 'typical of internal packages. This strongly suggests a dependency confusion attack.',
86
+ recommendation: 'Do NOT install this package without verifying it is not targeting your internal dependencies.',
87
+ });
88
+ }
89
+ // Check for recently published + few versions (suspicious new package)
90
+ if (isRecentlyPublished && context.registryMetadata.versions.length <= 2 && !hasOrgNaming) {
91
+ // Only flag if the name also looks somewhat organizational
92
+ if (this.hasOrgLookingSegments(lowerName)) {
93
+ findings.push({
94
+ analyzer: this.name,
95
+ severity: 'medium',
96
+ title: 'New package with organizational naming segments',
97
+ description: `The package "${packageName}" was recently published with few versions ` +
98
+ 'and contains segments that look like organizational identifiers.',
99
+ recommendation: 'Verify the package origin and ensure it is not a dependency confusion attempt.',
100
+ });
101
+ }
102
+ }
103
+ }
104
+ catch (error) {
105
+ logger.warn(`[${this.name}] Unexpected error: ${error instanceof Error ? error.message : String(error)}`);
106
+ }
107
+ return Promise.resolve({
108
+ analyzer: this.name,
109
+ findings,
110
+ duration: performance.now() - start,
111
+ });
112
+ }
113
+ isRecentlyPublished(context) {
114
+ const { publishedAt, timeMap, version } = context.registryMetadata;
115
+ // Try the specific version's publish time first
116
+ const versionTime = timeMap[version];
117
+ const timeStr = versionTime ?? publishedAt;
118
+ if (timeStr === undefined || timeStr === '')
119
+ return false;
120
+ const publishDate = new Date(timeStr);
121
+ const now = new Date();
122
+ const ageInDays = (now.getTime() - publishDate.getTime()) / (1000 * 60 * 60 * 24);
123
+ return ageInDays < RECENT_PUBLISH_DAYS;
124
+ }
125
+ hasOrgLookingSegments(name) {
126
+ const segments = name.split('-');
127
+ const orgIndicators = [
128
+ 'team',
129
+ 'org',
130
+ 'dept',
131
+ 'group',
132
+ 'division',
133
+ 'unit',
134
+ 'service',
135
+ 'svc',
136
+ 'api',
137
+ 'lib',
138
+ 'util',
139
+ 'utils',
140
+ 'common',
141
+ 'shared',
142
+ 'infra',
143
+ ];
144
+ return segments.some((segment) => orgIndicators.includes(segment));
145
+ }
146
+ }
147
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/analyzers/dependency-confusion/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Dormant Package Analyzer — detects packages that were reactivated after
3
+ * a long period of inactivity, which may indicate a hijacked or compromised package.
4
+ */
5
+ import type { Analyzer } from '../analyzer.interface.js';
6
+ import type { AnalysisContext, AnalyzerResult } from '../../core/types.js';
7
+ export declare class DormantPackageAnalyzer implements Analyzer {
8
+ readonly name = "dormant-package";
9
+ readonly description = "Detects packages reactivated after long periods of inactivity";
10
+ analyze(context: AnalysisContext): Promise<AnalyzerResult>;
11
+ private buildSortedVersionDates;
12
+ private checkHistoricalGaps;
13
+ }
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/analyzers/dormant-package/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,0BAA0B,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAW,MAAM,qBAAqB,CAAC;AAoBpF,qBAAa,sBAAuB,YAAW,QAAQ;IACrD,QAAQ,CAAC,IAAI,qBAAqB;IAClC,QAAQ,CAAC,WAAW,mEAAmE;IAEvF,OAAO,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC;IAmG1D,OAAO,CAAC,uBAAuB;IAqB/B,OAAO,CAAC,mBAAmB;CA8B5B"}
@@ -0,0 +1,137 @@
1
+ /**
2
+ * Dormant Package Analyzer — detects packages that were reactivated after
3
+ * a long period of inactivity, which may indicate a hijacked or compromised package.
4
+ */
5
+ import { logger } from '../../utils/logger.js';
6
+ // ─── Constants ────────────────────────────────────────────
7
+ /** Minimum gap in days to consider a package dormant. */
8
+ const DORMANCY_THRESHOLD_DAYS = 365;
9
+ /** Milliseconds per day. */
10
+ const MS_PER_DAY = 1000 * 60 * 60 * 24;
11
+ // ─── Analyzer ─────────────────────────────────────────────
12
+ export class DormantPackageAnalyzer {
13
+ name = 'dormant-package';
14
+ description = 'Detects packages reactivated after long periods of inactivity';
15
+ analyze(context) {
16
+ const start = performance.now();
17
+ const findings = [];
18
+ try {
19
+ const { versions, timeMap, maintainers } = context.registryMetadata;
20
+ if (versions.length < 2) {
21
+ // Need at least 2 versions to check for dormancy gaps
22
+ return Promise.resolve({
23
+ analyzer: this.name,
24
+ findings: [],
25
+ duration: performance.now() - start,
26
+ });
27
+ }
28
+ // Build a sorted list of (version, date) pairs
29
+ const versionDates = this.buildSortedVersionDates(versions, timeMap);
30
+ if (versionDates.length < 2) {
31
+ return Promise.resolve({
32
+ analyzer: this.name,
33
+ findings: [],
34
+ duration: performance.now() - start,
35
+ });
36
+ }
37
+ // Check the gap between the latest version and the one before it
38
+ const latest = versionDates[versionDates.length - 1];
39
+ const previous = versionDates[versionDates.length - 2];
40
+ if (!latest || !previous) {
41
+ return Promise.resolve({
42
+ analyzer: this.name,
43
+ findings: [],
44
+ duration: performance.now() - start,
45
+ });
46
+ }
47
+ const gapDays = Math.floor((latest.date.getTime() - previous.date.getTime()) / MS_PER_DAY);
48
+ if (gapDays > DORMANCY_THRESHOLD_DAYS) {
49
+ const gapMonths = Math.floor(gapDays / 30);
50
+ const gapYears = (gapDays / 365).toFixed(1);
51
+ // Check if we're looking at the current version
52
+ const isCurrentVersion = latest.version === context.version ||
53
+ latest.version === context.registryMetadata.distTags['latest'];
54
+ if (isCurrentVersion) {
55
+ findings.push({
56
+ analyzer: this.name,
57
+ severity: 'medium',
58
+ title: `Package reactivated after ${gapMonths} months of dormancy`,
59
+ description: `The latest version (${latest.version}) was published on ` +
60
+ `${latest.date.toISOString().split('T')[0] ?? 'unknown'} after a gap of ${gapDays} days ` +
61
+ `(~${gapYears} years) since the previous version (${previous.version}, ` +
62
+ `published ${previous.date.toISOString().split('T')[0] ?? 'unknown'}). ` +
63
+ 'Reactivation after long dormancy can indicate a hijacked package.',
64
+ recommendation: 'Review the changelog and recent commits. Verify the maintainer identity ' +
65
+ 'and check for unexpected changes in package behavior.',
66
+ });
67
+ // Check if maintainer list looks different (heuristic: very few maintainers
68
+ // combined with dormancy is extra suspicious)
69
+ if (maintainers.length === 1) {
70
+ findings.push({
71
+ analyzer: this.name,
72
+ severity: 'high',
73
+ title: 'Dormant package reactivated by single maintainer',
74
+ description: `The package was dormant for ${gapDays} days and is now maintained by a single person ` +
75
+ `(${maintainers[0]?.name ?? 'unknown'}). This pattern is consistent with package takeover.`,
76
+ recommendation: 'Verify the maintainer identity. Compare with previous version maintainers ' +
77
+ 'if possible. Consider alternatives if the package seems compromised.',
78
+ });
79
+ }
80
+ }
81
+ // Also check for large gaps anywhere in the version history
82
+ this.checkHistoricalGaps(versionDates, findings);
83
+ }
84
+ }
85
+ catch (error) {
86
+ logger.warn(`[${this.name}] Unexpected error: ${error instanceof Error ? error.message : String(error)}`);
87
+ }
88
+ return Promise.resolve({
89
+ analyzer: this.name,
90
+ findings,
91
+ duration: performance.now() - start,
92
+ });
93
+ }
94
+ buildSortedVersionDates(versions, timeMap) {
95
+ const result = [];
96
+ for (const version of versions) {
97
+ const timeStr = timeMap[version];
98
+ if (timeStr === undefined || timeStr === '')
99
+ continue;
100
+ const date = new Date(timeStr);
101
+ if (isNaN(date.getTime()))
102
+ continue;
103
+ result.push({ version, date });
104
+ }
105
+ // Sort by date ascending
106
+ result.sort((a, b) => a.date.getTime() - b.date.getTime());
107
+ return result;
108
+ }
109
+ checkHistoricalGaps(versionDates, findings) {
110
+ // Find the largest historical gap (excluding the latest, already checked above)
111
+ let maxGap = 0;
112
+ let maxGapStart;
113
+ let maxGapEnd;
114
+ for (let i = 1; i < versionDates.length - 1; i++) {
115
+ const prev = versionDates[i - 1];
116
+ const curr = versionDates[i];
117
+ if (!prev || !curr)
118
+ continue;
119
+ const gap = (curr.date.getTime() - prev.date.getTime()) / MS_PER_DAY;
120
+ if (gap > maxGap) {
121
+ maxGap = gap;
122
+ maxGapStart = prev;
123
+ maxGapEnd = curr;
124
+ }
125
+ }
126
+ if (maxGap > DORMANCY_THRESHOLD_DAYS * 2 && maxGapStart && maxGapEnd) {
127
+ findings.push({
128
+ analyzer: this.name,
129
+ severity: 'info',
130
+ title: `Historical dormancy gap: ${Math.floor(maxGap)} days`,
131
+ description: `Between versions ${maxGapStart.version} and ${maxGapEnd.version}, ` +
132
+ `there was a gap of ${Math.floor(maxGap)} days.`,
133
+ });
134
+ }
135
+ }
136
+ }
137
+ //# sourceMappingURL=index.js.map