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.
- package/LICENSE +21 -0
- package/README.md +193 -0
- package/dist/analyzers/analyzer.interface.d.ts +32 -0
- package/dist/analyzers/analyzer.interface.d.ts.map +1 -0
- package/dist/analyzers/analyzer.interface.js +2 -0
- package/dist/analyzers/analyzer.interface.js.map +1 -0
- package/dist/analyzers/analyzer.registry.d.ts +16 -0
- package/dist/analyzers/analyzer.registry.d.ts.map +1 -0
- package/dist/analyzers/analyzer.registry.js +40 -0
- package/dist/analyzers/analyzer.registry.js.map +1 -0
- package/dist/analyzers/dependency-confusion/index.d.ts +14 -0
- package/dist/analyzers/dependency-confusion/index.d.ts.map +1 -0
- package/dist/analyzers/dependency-confusion/index.js +147 -0
- package/dist/analyzers/dependency-confusion/index.js.map +1 -0
- package/dist/analyzers/dormant-package/index.d.ts +14 -0
- package/dist/analyzers/dormant-package/index.d.ts.map +1 -0
- package/dist/analyzers/dormant-package/index.js +137 -0
- package/dist/analyzers/dormant-package/index.js.map +1 -0
- package/dist/analyzers/file-based-analyzer.d.ts +20 -0
- package/dist/analyzers/file-based-analyzer.d.ts.map +1 -0
- package/dist/analyzers/file-based-analyzer.js +35 -0
- package/dist/analyzers/file-based-analyzer.js.map +1 -0
- package/dist/analyzers/install-scripts/index.d.ts +13 -0
- package/dist/analyzers/install-scripts/index.d.ts.map +1 -0
- package/dist/analyzers/install-scripts/index.js +125 -0
- package/dist/analyzers/install-scripts/index.js.map +1 -0
- package/dist/analyzers/license/index.d.ts +12 -0
- package/dist/analyzers/license/index.d.ts.map +1 -0
- package/dist/analyzers/license/index.js +199 -0
- package/dist/analyzers/license/index.js.map +1 -0
- package/dist/analyzers/maintainer/index.d.ts +12 -0
- package/dist/analyzers/maintainer/index.d.ts.map +1 -0
- package/dist/analyzers/maintainer/index.js +93 -0
- package/dist/analyzers/maintainer/index.js.map +1 -0
- package/dist/analyzers/network-calls/index.d.ts +15 -0
- package/dist/analyzers/network-calls/index.d.ts.map +1 -0
- package/dist/analyzers/network-calls/index.js +212 -0
- package/dist/analyzers/network-calls/index.js.map +1 -0
- package/dist/analyzers/obfuscation/index.d.ts +19 -0
- package/dist/analyzers/obfuscation/index.d.ts.map +1 -0
- package/dist/analyzers/obfuscation/index.js +218 -0
- package/dist/analyzers/obfuscation/index.js.map +1 -0
- package/dist/analyzers/prototype-pollution/index.d.ts +18 -0
- package/dist/analyzers/prototype-pollution/index.d.ts.map +1 -0
- package/dist/analyzers/prototype-pollution/index.js +257 -0
- package/dist/analyzers/prototype-pollution/index.js.map +1 -0
- package/dist/analyzers/sensitive-data/index.d.ts +16 -0
- package/dist/analyzers/sensitive-data/index.d.ts.map +1 -0
- package/dist/analyzers/sensitive-data/index.js +254 -0
- package/dist/analyzers/sensitive-data/index.js.map +1 -0
- package/dist/analyzers/typosquatting/index.d.ts +14 -0
- package/dist/analyzers/typosquatting/index.d.ts.map +1 -0
- package/dist/analyzers/typosquatting/index.js +127 -0
- package/dist/analyzers/typosquatting/index.js.map +1 -0
- package/dist/analyzers/typosquatting/popular-packages.d.ts +9 -0
- package/dist/analyzers/typosquatting/popular-packages.d.ts.map +1 -0
- package/dist/analyzers/typosquatting/popular-packages.js +236 -0
- package/dist/analyzers/typosquatting/popular-packages.js.map +1 -0
- package/dist/analyzers/vulnerability/index.d.ts +12 -0
- package/dist/analyzers/vulnerability/index.d.ts.map +1 -0
- package/dist/analyzers/vulnerability/index.js +147 -0
- package/dist/analyzers/vulnerability/index.js.map +1 -0
- package/dist/cli/commands/check.d.ts +21 -0
- package/dist/cli/commands/check.d.ts.map +1 -0
- package/dist/cli/commands/check.js +204 -0
- package/dist/cli/commands/check.js.map +1 -0
- package/dist/cli/formatters/formatter.interface.d.ts +14 -0
- package/dist/cli/formatters/formatter.interface.d.ts.map +1 -0
- package/dist/cli/formatters/formatter.interface.js +2 -0
- package/dist/cli/formatters/formatter.interface.js.map +1 -0
- package/dist/cli/formatters/json.d.ts +12 -0
- package/dist/cli/formatters/json.d.ts.map +1 -0
- package/dist/cli/formatters/json.js +12 -0
- package/dist/cli/formatters/json.js.map +1 -0
- package/dist/cli/formatters/sarif.d.ts +13 -0
- package/dist/cli/formatters/sarif.d.ts.map +1 -0
- package/dist/cli/formatters/sarif.js +101 -0
- package/dist/cli/formatters/sarif.js.map +1 -0
- package/dist/cli/formatters/terminal.d.ts +13 -0
- package/dist/cli/formatters/terminal.d.ts.map +1 -0
- package/dist/cli/formatters/terminal.js +110 -0
- package/dist/cli/formatters/terminal.js.map +1 -0
- package/dist/cli/index.d.ts +9 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +86 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/core/engine.d.ts +23 -0
- package/dist/core/engine.d.ts.map +1 -0
- package/dist/core/engine.js +55 -0
- package/dist/core/engine.js.map +1 -0
- package/dist/core/scorer.d.ts +30 -0
- package/dist/core/scorer.d.ts.map +1 -0
- package/dist/core/scorer.js +88 -0
- package/dist/core/scorer.js.map +1 -0
- package/dist/core/types.d.ts +76 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +30 -0
- package/dist/core/types.js.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/registry/client.d.ts +27 -0
- package/dist/registry/client.d.ts.map +1 -0
- package/dist/registry/client.js +189 -0
- package/dist/registry/client.js.map +1 -0
- package/dist/registry/tarball.d.ts +34 -0
- package/dist/registry/tarball.d.ts.map +1 -0
- package/dist/registry/tarball.js +103 -0
- package/dist/registry/tarball.js.map +1 -0
- package/dist/utils/ast.d.ts +74 -0
- package/dist/utils/ast.d.ts.map +1 -0
- package/dist/utils/ast.js +150 -0
- package/dist/utils/ast.js.map +1 -0
- package/dist/utils/fs.d.ts +28 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +78 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/http.d.ts +40 -0
- package/dist/utils/http.d.ts.map +1 -0
- package/dist/utils/http.js +116 -0
- package/dist/utils/http.js.map +1 -0
- package/dist/utils/logger.d.ts +46 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +91 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/strings.d.ts +8 -0
- package/dist/utils/strings.d.ts.map +1 -0
- package/dist/utils/strings.js +12 -0
- package/dist/utils/strings.js.map +1 -0
- 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 @@
|
|
|
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
|