tangkal 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +62 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/src/analyzers/dependencies.d.ts +4 -0
- package/dist/src/analyzers/dependencies.d.ts.map +1 -0
- package/dist/src/analyzers/dependencies.js +79 -0
- package/dist/src/analyzers/dependencies.js.map +1 -0
- package/dist/src/analyzers/network.d.ts +17 -0
- package/dist/src/analyzers/network.d.ts.map +1 -0
- package/dist/src/analyzers/network.js +203 -0
- package/dist/src/analyzers/network.js.map +1 -0
- package/dist/src/analyzers/static-analysis.d.ts +18 -0
- package/dist/src/analyzers/static-analysis.d.ts.map +1 -0
- package/dist/src/analyzers/static-analysis.js +246 -0
- package/dist/src/analyzers/static-analysis.js.map +1 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.d.ts.map +1 -0
- package/dist/src/cli.js +112 -0
- package/dist/src/cli.js.map +1 -0
- package/dist/src/config.d.ts +9 -0
- package/dist/src/config.d.ts.map +1 -0
- package/dist/src/config.js +40 -0
- package/dist/src/config.js.map +1 -0
- package/dist/src/scanner.d.ts +8 -0
- package/dist/src/scanner.d.ts.map +1 -0
- package/dist/src/scanner.js +115 -0
- package/dist/src/scanner.js.map +1 -0
- package/dist/src/utils/entropy.d.ts +7 -0
- package/dist/src/utils/entropy.d.ts.map +1 -0
- package/dist/src/utils/entropy.js +25 -0
- package/dist/src/utils/entropy.js.map +1 -0
- package/dist/src/utils/ignore.d.ts +3 -0
- package/dist/src/utils/ignore.d.ts.map +1 -0
- package/dist/src/utils/ignore.js +18 -0
- package/dist/src/utils/ignore.js.map +1 -0
- package/dist/src/utils/lockfile.d.ts +9 -0
- package/dist/src/utils/lockfile.d.ts.map +1 -0
- package/dist/src/utils/lockfile.js +143 -0
- package/dist/src/utils/lockfile.js.map +1 -0
- package/dist/src/utils/popular-packages.d.ts +2 -0
- package/dist/src/utils/popular-packages.d.ts.map +1 -0
- package/dist/src/utils/popular-packages.js +22 -0
- package/dist/src/utils/popular-packages.js.map +1 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Tangkal 🛡️
|
|
2
|
+
|
|
3
|
+
**Tangkal** (Indonesian for "ward off" or "repel") is a lightweight, preventive security scanner designed to inspect cloned repositories *before* you run `npm install`.
|
|
4
|
+
|
|
5
|
+
It is specifically built to detect malicious patterns often found in "Job Scam" repositories, such as:
|
|
6
|
+
- **Obfuscated Code:** Base64 (atob, Buffer), Hexadecimal strings.
|
|
7
|
+
- **Dynamic Execution:** `eval`, `new Function`.
|
|
8
|
+
- **Hidden Network Calls:** Fetching payloads from remote URLs (e.g., JSON keepers).
|
|
9
|
+
- **Dangerous Lifecycle Scripts:** `preinstall`, `postinstall` in `package.json`.
|
|
10
|
+
- **Typosquatting:** Detects packages with names deceptively similar to popular libraries (e.g., `react-doom` vs `react-dom`).
|
|
11
|
+
- **Vulnerability Scanning:** Aggregates data from **OSV**, **Snyk**, and **Exploit DB** to report known vulnerabilities.
|
|
12
|
+
|
|
13
|
+
## Installation
|
|
14
|
+
|
|
15
|
+
### From Source
|
|
16
|
+
```bash
|
|
17
|
+
git clone https://github.com/yourusername/tangkal.git
|
|
18
|
+
cd tangkal
|
|
19
|
+
npm install
|
|
20
|
+
npm link
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Usage
|
|
24
|
+
|
|
25
|
+
Run `tangkal` against any suspicious directory:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
tangkal ./path-to-suspicious-repo
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Or simply inside the directory:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
cd suspicious-repo
|
|
35
|
+
tangkal .
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Output Example
|
|
39
|
+
|
|
40
|
+
Tangkal separates findings into two clear categories: **Malicious Code** and **Vulnerable Packages**.
|
|
41
|
+
|
|
42
|
+
```text
|
|
43
|
+
====================================
|
|
44
|
+
ALERT: Malicious Code Detected
|
|
45
|
+
====================================
|
|
46
|
+
File: src/utils.js
|
|
47
|
+
Line: 45
|
|
48
|
+
Suspicious pattern detected.
|
|
49
|
+
Code: new Function("return " + decodedPayload)()
|
|
50
|
+
|
|
51
|
+
====================================
|
|
52
|
+
ALERT: Vulnerable Package
|
|
53
|
+
====================================
|
|
54
|
+
[SOLUTION]: Upgrade lodash@4.17.15 to lodash@4.17.21 to fix.
|
|
55
|
+
[HIGH Severity] [https://osv.dev/vulnerability/GHSA-xxx] [Snyk: https://security.snyk.io/vuln?search=CVE-2021-23337]
|
|
56
|
+
lodash@4.17.15 Prototype Pollution
|
|
57
|
+
introduced by lodash@4.17.15
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Disclaimer
|
|
61
|
+
|
|
62
|
+
This tool uses heuristic pattern matching. It may produce false positives (e.g., in build scripts or test files) and cannot guarantee 100% safety. **Always review code manually if you are unsure.**
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,GAAG,EAAE,MAAM,cAAc,CAAC;AAEnC,GAAG,EAAE,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dependencies.d.ts","sourceRoot":"","sources":["../../../src/analyzers/dependencies.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AAIpD,wBAAsB,kBAAkB,CAAC,OAAO,EAAE,GAAG,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CA4BzE;AAED,wBAAsB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CA2C1E"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import levenshtein from 'fast-levenshtein';
|
|
2
|
+
import { getPopularPackages } from '../utils/popular-packages.js';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
const execAsync = promisify(exec);
|
|
6
|
+
export async function checkTyposquatting(pkgJson) {
|
|
7
|
+
const findings = [];
|
|
8
|
+
const allDeps = {
|
|
9
|
+
...pkgJson.dependencies,
|
|
10
|
+
...pkgJson.devDependencies
|
|
11
|
+
};
|
|
12
|
+
const popularPackages = await getPopularPackages();
|
|
13
|
+
for (const depName of Object.keys(allDeps)) {
|
|
14
|
+
for (const popular of popularPackages) {
|
|
15
|
+
if (depName === popular)
|
|
16
|
+
continue; // Exact match is fine
|
|
17
|
+
const distance = levenshtein.get(depName, popular);
|
|
18
|
+
const threshold = popular.length < 5 ? 1 : 2;
|
|
19
|
+
if (distance <= threshold) {
|
|
20
|
+
findings.push({
|
|
21
|
+
type: 'Typosquatting',
|
|
22
|
+
name: depName,
|
|
23
|
+
file: 'package.json', // Placeholder, caller handles file
|
|
24
|
+
severity: 'high',
|
|
25
|
+
description: `Package '${depName}' looks very similar to popular package '${popular}'.`
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return findings;
|
|
31
|
+
}
|
|
32
|
+
export async function checkVulnerabilities(cwd) {
|
|
33
|
+
try {
|
|
34
|
+
const { stdout } = await execAsync('npm audit --json --audit-level=moderate', { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
35
|
+
const auditResult = JSON.parse(stdout);
|
|
36
|
+
const vulns = [];
|
|
37
|
+
if (auditResult.vulnerabilities) {
|
|
38
|
+
for (const [name, info] of Object.entries(auditResult.vulnerabilities)) {
|
|
39
|
+
if (info.severity === 'high' || info.severity === 'critical') {
|
|
40
|
+
vulns.push({
|
|
41
|
+
type: 'Vulnerability',
|
|
42
|
+
name,
|
|
43
|
+
file: 'package-lock.json',
|
|
44
|
+
severity: info.severity,
|
|
45
|
+
description: `Known vulnerability via ${info.via && info.via.join ? info.via.join(', ') : 'transitive dependency'}`
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return vulns;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error.stdout) {
|
|
54
|
+
try {
|
|
55
|
+
const auditResult = JSON.parse(error.stdout);
|
|
56
|
+
const vulns = [];
|
|
57
|
+
if (auditResult.vulnerabilities) {
|
|
58
|
+
for (const [name, info] of Object.entries(auditResult.vulnerabilities)) {
|
|
59
|
+
if (info.severity === 'high' || info.severity === 'critical') {
|
|
60
|
+
vulns.push({
|
|
61
|
+
type: 'Vulnerability',
|
|
62
|
+
name,
|
|
63
|
+
file: 'package-lock.json',
|
|
64
|
+
severity: info.severity,
|
|
65
|
+
description: `Known vulnerability (Severity: ${info.severity})`
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return vulns;
|
|
71
|
+
}
|
|
72
|
+
catch (e) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
return [];
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=dependencies.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dependencies.js","sourceRoot":"","sources":["../../../src/analyzers/dependencies.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { Finding } from './static-analysis.js';
|
|
2
|
+
import type { Dependency } from '../utils/lockfile.js';
|
|
3
|
+
/**
|
|
4
|
+
* Main entry point for network audit.
|
|
5
|
+
* Checks vulnerabilities for all packages.
|
|
6
|
+
* Checks reputation for a subset (or all if count is low) to avoid rate limiting.
|
|
7
|
+
*/
|
|
8
|
+
export declare function auditDependencies(packages: Dependency[]): Promise<Finding[]>;
|
|
9
|
+
/**
|
|
10
|
+
* Checks the npm registry for package reputation (downloads, age).
|
|
11
|
+
*/
|
|
12
|
+
export declare function checkReputation(pkgName: string): Promise<Finding[]>;
|
|
13
|
+
/**
|
|
14
|
+
* Checks for known vulnerabilities using OSV API (Batch Mode).
|
|
15
|
+
*/
|
|
16
|
+
export declare function checkVulnerabilitiesBatch(packages: Dependency[]): Promise<Finding[]>;
|
|
17
|
+
//# sourceMappingURL=network.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.d.ts","sourceRoot":"","sources":["../../../src/analyzers/network.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,sBAAsB,CAAC;AACpD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAIvD;;;;GAIG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAuBlF;AAED;;GAEG;AACH,wBAAsB,eAAe,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CA2DzE;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAAC,QAAQ,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CAsH1F"}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import pLimit from 'p-limit';
|
|
3
|
+
const limit = pLimit(10); // Increased concurrency for network checks
|
|
4
|
+
/**
|
|
5
|
+
* Main entry point for network audit.
|
|
6
|
+
* Checks vulnerabilities for all packages.
|
|
7
|
+
* Checks reputation for a subset (or all if count is low) to avoid rate limiting.
|
|
8
|
+
*/
|
|
9
|
+
export async function auditDependencies(packages) {
|
|
10
|
+
const findings = [];
|
|
11
|
+
// 1. Vulnerabilities (Batch API - Fast)
|
|
12
|
+
try {
|
|
13
|
+
const vulnFindings = await checkVulnerabilitiesBatch(packages);
|
|
14
|
+
findings.push(...vulnFindings);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
console.error('Vulnerability check failed:', e);
|
|
18
|
+
}
|
|
19
|
+
// 2. Reputation (Per-package API - Slow)
|
|
20
|
+
// Only check reputation if < 200 packages to avoid long wait times
|
|
21
|
+
if (packages.length < 200) {
|
|
22
|
+
const reputationPromises = packages.map(p => checkReputation(p.name));
|
|
23
|
+
const repResults = await Promise.all(reputationPromises);
|
|
24
|
+
repResults.forEach(r => findings.push(...r));
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
// For large trees, maybe only check random sample?
|
|
28
|
+
// Or just skip to be safe.
|
|
29
|
+
}
|
|
30
|
+
return findings;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Checks the npm registry for package reputation (downloads, age).
|
|
34
|
+
*/
|
|
35
|
+
export async function checkReputation(pkgName) {
|
|
36
|
+
return limit(async () => {
|
|
37
|
+
try {
|
|
38
|
+
// 1. Get Metadata (Time, Maintainers)
|
|
39
|
+
const regUrl = `https://registry.npmjs.org/${pkgName}`;
|
|
40
|
+
const { data: meta } = await axios.get(regUrl, { timeout: 3000 });
|
|
41
|
+
const findings = [];
|
|
42
|
+
const now = new Date();
|
|
43
|
+
const created = new Date(meta.time.created);
|
|
44
|
+
const ageDays = (now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24);
|
|
45
|
+
if (ageDays < 14) {
|
|
46
|
+
findings.push({
|
|
47
|
+
type: 'Reputation',
|
|
48
|
+
name: pkgName,
|
|
49
|
+
file: 'package.json',
|
|
50
|
+
severity: 'high',
|
|
51
|
+
description: `Package is brand new (created ${Math.round(ageDays)} days ago).`
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// 2. Get Downloads (Popularity)
|
|
55
|
+
try {
|
|
56
|
+
const dlUrl = `https://api.npmjs.org/downloads/point/last-week/${pkgName}`;
|
|
57
|
+
const { data: dl } = await axios.get(dlUrl, { timeout: 3000 });
|
|
58
|
+
if (dl.downloads < 50) {
|
|
59
|
+
findings.push({
|
|
60
|
+
type: 'Reputation',
|
|
61
|
+
name: pkgName,
|
|
62
|
+
file: 'package.json',
|
|
63
|
+
severity: 'medium',
|
|
64
|
+
description: `Extremely low downloads (${dl.downloads}/week). Potential typosquat or abandoned.`
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
// Download stats might be private/unavailable
|
|
70
|
+
}
|
|
71
|
+
return findings;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error.response && error.response.status === 404) {
|
|
75
|
+
const isScoped = pkgName.startsWith('@');
|
|
76
|
+
return [{
|
|
77
|
+
type: 'Reputation',
|
|
78
|
+
name: pkgName,
|
|
79
|
+
file: 'package.json',
|
|
80
|
+
severity: isScoped ? 'low' : 'critical',
|
|
81
|
+
description: isScoped
|
|
82
|
+
? 'Scoped package not found in public registry (Likely Private).'
|
|
83
|
+
: 'Unscoped package not found in registry (Possible Malware/Typosquat).'
|
|
84
|
+
}];
|
|
85
|
+
}
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Checks for known vulnerabilities using OSV API (Batch Mode).
|
|
92
|
+
*/
|
|
93
|
+
export async function checkVulnerabilitiesBatch(packages) {
|
|
94
|
+
if (packages.length === 0)
|
|
95
|
+
return [];
|
|
96
|
+
const chunkSize = 500;
|
|
97
|
+
const chunks = [];
|
|
98
|
+
for (let i = 0; i < packages.length; i += chunkSize) {
|
|
99
|
+
chunks.push(packages.slice(i, i + chunkSize));
|
|
100
|
+
}
|
|
101
|
+
const allVulns = [];
|
|
102
|
+
for (const chunk of chunks) {
|
|
103
|
+
try {
|
|
104
|
+
const payload = {
|
|
105
|
+
queries: chunk.map(p => ({
|
|
106
|
+
package: { name: p.name, ecosystem: 'npm' },
|
|
107
|
+
version: p.version
|
|
108
|
+
}))
|
|
109
|
+
};
|
|
110
|
+
const { data } = await axios.post('https://api.osv.dev/v1/querybatch', payload, { timeout: 10000 });
|
|
111
|
+
const vulnIds = new Set();
|
|
112
|
+
data.results.forEach((res) => {
|
|
113
|
+
if (res.vulns) {
|
|
114
|
+
res.vulns.forEach((v) => vulnIds.add(v.id));
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const detailsMap = new Map();
|
|
118
|
+
const detailPromises = Array.from(vulnIds).map(id => limit(async () => {
|
|
119
|
+
try {
|
|
120
|
+
const { data: detail } = await axios.get(`https://api.osv.dev/v1/vulns/${id}`, { timeout: 5000 });
|
|
121
|
+
detailsMap.set(id, detail);
|
|
122
|
+
}
|
|
123
|
+
catch (e) { }
|
|
124
|
+
}));
|
|
125
|
+
await Promise.all(detailPromises);
|
|
126
|
+
data.results.forEach((res, idx) => {
|
|
127
|
+
if (res.vulns && res.vulns.length > 0) {
|
|
128
|
+
const pkg = chunk[idx];
|
|
129
|
+
if (!pkg)
|
|
130
|
+
return;
|
|
131
|
+
res.vulns.forEach((basicV) => {
|
|
132
|
+
const v = detailsMap.get(basicV.id) || basicV;
|
|
133
|
+
let fixedIn = 'Unknown';
|
|
134
|
+
if (v.affected) {
|
|
135
|
+
for (const affected of v.affected) {
|
|
136
|
+
if (affected.ranges) {
|
|
137
|
+
for (const range of affected.ranges) {
|
|
138
|
+
if (range.events) {
|
|
139
|
+
for (const event of range.events) {
|
|
140
|
+
if (event.fixed) {
|
|
141
|
+
if (fixedIn === 'Unknown' || event.fixed > fixedIn) {
|
|
142
|
+
fixedIn = event.fixed;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let severity = 'high';
|
|
152
|
+
if (v.database_specific && v.database_specific.severity) {
|
|
153
|
+
severity = v.database_specific.severity.toLowerCase();
|
|
154
|
+
}
|
|
155
|
+
else if (v.severity && v.severity.length > 0) {
|
|
156
|
+
severity = 'high';
|
|
157
|
+
}
|
|
158
|
+
else {
|
|
159
|
+
const text = JSON.stringify(v).toLowerCase();
|
|
160
|
+
if (text.includes('critical'))
|
|
161
|
+
severity = 'critical';
|
|
162
|
+
else if (text.includes('high'))
|
|
163
|
+
severity = 'high';
|
|
164
|
+
else if (text.includes('medium'))
|
|
165
|
+
severity = 'medium';
|
|
166
|
+
else
|
|
167
|
+
severity = 'low';
|
|
168
|
+
}
|
|
169
|
+
const externalRefs = [];
|
|
170
|
+
if (v.aliases) {
|
|
171
|
+
v.aliases.forEach((alias) => {
|
|
172
|
+
if (alias.startsWith('CVE-')) {
|
|
173
|
+
externalRefs.push(`Snyk: https://security.snyk.io/vuln?search=${alias}`);
|
|
174
|
+
externalRefs.push(`ExploitDB: https://www.exploit-db.com/search?cve=${alias.replace('CVE-', '')}`);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
const osvUrl = `https://osv.dev/vulnerability/${v.id}`;
|
|
179
|
+
const summary = v.summary || v.details?.split('\n')[0] || 'Vulnerability detected';
|
|
180
|
+
allVulns.push({
|
|
181
|
+
type: 'Vulnerability',
|
|
182
|
+
name: pkg.name,
|
|
183
|
+
version: pkg.version,
|
|
184
|
+
severity: severity,
|
|
185
|
+
file: 'package-lock.json',
|
|
186
|
+
id: v.id,
|
|
187
|
+
summary: summary,
|
|
188
|
+
url: osvUrl,
|
|
189
|
+
fixedIn: fixedIn,
|
|
190
|
+
description: summary,
|
|
191
|
+
references: externalRefs
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch (e) {
|
|
198
|
+
console.error(e);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return allVulns;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=network.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"network.js","sourceRoot":"","sources":["../../../src/analyzers/network.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface Finding {
|
|
2
|
+
type: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
file: string;
|
|
5
|
+
line?: number;
|
|
6
|
+
severity: 'critical' | 'high' | 'medium' | 'low';
|
|
7
|
+
content?: string;
|
|
8
|
+
description: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
id?: string;
|
|
11
|
+
summary?: string;
|
|
12
|
+
url?: string;
|
|
13
|
+
fixedIn?: string;
|
|
14
|
+
references?: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare function analyzeStream(filePath: string): Promise<Finding[]>;
|
|
17
|
+
export declare function analyzeContent(content: string, file: string): Finding[];
|
|
18
|
+
//# sourceMappingURL=static-analysis.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-analysis.d.ts","sourceRoot":"","sources":["../../../src/analyzers/static-analysis.ts"],"names":[],"mappings":"AASA,MAAM,WAAW,OAAO;IACtB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,UAAU,GAAG,MAAM,GAAG,QAAQ,GAAG,KAAK,CAAC;IACjD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;CACvB;AAED,wBAAsB,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,EAAE,CAAC,CA4CxE;AAED,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,EAAE,CA+MvE"}
|