guardrail-security 1.0.2 → 2.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/dist/sbom/generator.d.ts +42 -0
- package/dist/sbom/generator.d.ts.map +1 -1
- package/dist/sbom/generator.js +168 -7
- package/dist/secrets/allowlist.d.ts +38 -0
- package/dist/secrets/allowlist.d.ts.map +1 -0
- package/dist/secrets/allowlist.js +131 -0
- package/dist/secrets/config-loader.d.ts +25 -0
- package/dist/secrets/config-loader.d.ts.map +1 -0
- package/dist/secrets/config-loader.js +103 -0
- package/dist/secrets/contextual-risk.d.ts +19 -0
- package/dist/secrets/contextual-risk.d.ts.map +1 -0
- package/dist/secrets/contextual-risk.js +88 -0
- package/dist/secrets/git-scanner.d.ts +29 -0
- package/dist/secrets/git-scanner.d.ts.map +1 -0
- package/dist/secrets/git-scanner.js +109 -0
- package/dist/secrets/guardian.d.ts +70 -57
- package/dist/secrets/guardian.d.ts.map +1 -1
- package/dist/secrets/guardian.js +531 -258
- package/dist/secrets/index.d.ts +4 -0
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +11 -1
- package/dist/secrets/patterns.d.ts +39 -10
- package/dist/secrets/patterns.d.ts.map +1 -1
- package/dist/secrets/patterns.js +129 -71
- package/dist/secrets/pre-commit.d.ts.map +1 -1
- package/dist/secrets/pre-commit.js +1 -1
- package/dist/secrets/vault-integration.d.ts.map +1 -1
- package/dist/secrets/vault-integration.js +1 -0
- package/dist/supply-chain/vulnerability-db.d.ts +89 -16
- package/dist/supply-chain/vulnerability-db.d.ts.map +1 -1
- package/dist/supply-chain/vulnerability-db.js +404 -115
- package/dist/utils/semver.d.ts +37 -0
- package/dist/utils/semver.d.ts.map +1 -0
- package/dist/utils/semver.js +109 -0
- package/package.json +17 -3
- package/src/__tests__/license/engine.test.ts +0 -250
- package/src/__tests__/supply-chain/typosquat.test.ts +0 -191
- package/src/attack-surface/analyzer.ts +0 -153
- package/src/attack-surface/index.ts +0 -5
- package/src/index.ts +0 -21
- package/src/languages/index.ts +0 -91
- package/src/languages/java-analyzer.ts +0 -490
- package/src/languages/python-analyzer.ts +0 -498
- package/src/license/compatibility-matrix.ts +0 -366
- package/src/license/engine.ts +0 -346
- package/src/license/index.ts +0 -6
- package/src/sbom/generator.ts +0 -355
- package/src/sbom/index.ts +0 -5
- package/src/secrets/guardian.ts +0 -468
- package/src/secrets/index.ts +0 -10
- package/src/secrets/patterns.ts +0 -186
- package/src/secrets/pre-commit.ts +0 -158
- package/src/secrets/vault-integration.ts +0 -360
- package/src/secrets/vault-providers.ts +0 -446
- package/src/supply-chain/detector.ts +0 -253
- package/src/supply-chain/index.ts +0 -11
- package/src/supply-chain/malicious-db.ts +0 -103
- package/src/supply-chain/script-analyzer.ts +0 -194
- package/src/supply-chain/typosquat.ts +0 -302
- package/src/supply-chain/vulnerability-db.ts +0 -386
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lightweight Semver Utilities
|
|
3
|
+
* Proper version comparison for vulnerability checking
|
|
4
|
+
* (Avoids incorrect lexicographic comparison like "10.0.0" < "2.0.0")
|
|
5
|
+
*/
|
|
6
|
+
export interface SemverParts {
|
|
7
|
+
major: number;
|
|
8
|
+
minor: number;
|
|
9
|
+
patch: number;
|
|
10
|
+
prerelease?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Parse a semver string into components
|
|
14
|
+
* Handles formats: 1.2.3, 1.2.3-beta.1, ^1.2.3, ~1.2.3
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseSemver(version: string): SemverParts | null;
|
|
17
|
+
/**
|
|
18
|
+
* Compare two semver versions
|
|
19
|
+
* Returns: -1 if a < b, 0 if a == b, 1 if a > b
|
|
20
|
+
*/
|
|
21
|
+
export declare function compareSemver(a: string, b: string): number;
|
|
22
|
+
/**
|
|
23
|
+
* Check if version is less than target
|
|
24
|
+
* Enterprise-grade: "10.0.0" is NOT less than "2.0.0"
|
|
25
|
+
*/
|
|
26
|
+
export declare function isVersionLessThan(version: string, target: string): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Check if version satisfies a range expression
|
|
29
|
+
* Supports: <1.2.3, <=1.2.3, >1.2.3, >=1.2.3, 1.2.3 (exact)
|
|
30
|
+
*/
|
|
31
|
+
export declare function satisfiesRange(version: string, range: string): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Check if version is affected by vulnerability
|
|
34
|
+
* affectedVersions format: "<4.17.21" or ">=1.0.0 <2.0.0"
|
|
35
|
+
*/
|
|
36
|
+
export declare function isAffected(version: string, affectedVersions: string): boolean;
|
|
37
|
+
//# sourceMappingURL=semver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semver.d.ts","sourceRoot":"","sources":["../../src/utils/semver.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAyB/D;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CA4B1D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAE1E;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAkBtE;AAED;;;GAGG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAM7E"}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Lightweight Semver Utilities
|
|
4
|
+
* Proper version comparison for vulnerability checking
|
|
5
|
+
* (Avoids incorrect lexicographic comparison like "10.0.0" < "2.0.0")
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.parseSemver = parseSemver;
|
|
9
|
+
exports.compareSemver = compareSemver;
|
|
10
|
+
exports.isVersionLessThan = isVersionLessThan;
|
|
11
|
+
exports.satisfiesRange = satisfiesRange;
|
|
12
|
+
exports.isAffected = isAffected;
|
|
13
|
+
/**
|
|
14
|
+
* Parse a semver string into components
|
|
15
|
+
* Handles formats: 1.2.3, 1.2.3-beta.1, ^1.2.3, ~1.2.3
|
|
16
|
+
*/
|
|
17
|
+
function parseSemver(version) {
|
|
18
|
+
// Strip range prefixes
|
|
19
|
+
const cleaned = version.replace(/^[\^~>=<]+/, '').trim();
|
|
20
|
+
// Match semver pattern
|
|
21
|
+
const match = cleaned.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
|
|
22
|
+
if (!match) {
|
|
23
|
+
// Try partial versions (1.2, 1)
|
|
24
|
+
const partial = cleaned.match(/^(\d+)(?:\.(\d+))?$/);
|
|
25
|
+
if (partial) {
|
|
26
|
+
return {
|
|
27
|
+
major: parseInt(partial[1] ?? '0', 10),
|
|
28
|
+
minor: partial[2] ? parseInt(partial[2], 10) : 0,
|
|
29
|
+
patch: 0,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
major: parseInt(match[1] ?? '0', 10),
|
|
36
|
+
minor: parseInt(match[2] ?? '0', 10),
|
|
37
|
+
patch: parseInt(match[3] ?? '0', 10),
|
|
38
|
+
prerelease: match[4],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Compare two semver versions
|
|
43
|
+
* Returns: -1 if a < b, 0 if a == b, 1 if a > b
|
|
44
|
+
*/
|
|
45
|
+
function compareSemver(a, b) {
|
|
46
|
+
const parsedA = parseSemver(a);
|
|
47
|
+
const parsedB = parseSemver(b);
|
|
48
|
+
if (!parsedA || !parsedB) {
|
|
49
|
+
// Fallback to string comparison if parsing fails
|
|
50
|
+
return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
|
|
51
|
+
}
|
|
52
|
+
// Compare major.minor.patch
|
|
53
|
+
if (parsedA.major !== parsedB.major) {
|
|
54
|
+
return parsedA.major < parsedB.major ? -1 : 1;
|
|
55
|
+
}
|
|
56
|
+
if (parsedA.minor !== parsedB.minor) {
|
|
57
|
+
return parsedA.minor < parsedB.minor ? -1 : 1;
|
|
58
|
+
}
|
|
59
|
+
if (parsedA.patch !== parsedB.patch) {
|
|
60
|
+
return parsedA.patch < parsedB.patch ? -1 : 1;
|
|
61
|
+
}
|
|
62
|
+
// Handle prerelease (1.0.0-alpha < 1.0.0)
|
|
63
|
+
if (parsedA.prerelease && !parsedB.prerelease)
|
|
64
|
+
return -1;
|
|
65
|
+
if (!parsedA.prerelease && parsedB.prerelease)
|
|
66
|
+
return 1;
|
|
67
|
+
if (parsedA.prerelease && parsedB.prerelease) {
|
|
68
|
+
return parsedA.prerelease.localeCompare(parsedB.prerelease);
|
|
69
|
+
}
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Check if version is less than target
|
|
74
|
+
* Enterprise-grade: "10.0.0" is NOT less than "2.0.0"
|
|
75
|
+
*/
|
|
76
|
+
function isVersionLessThan(version, target) {
|
|
77
|
+
return compareSemver(version, target) < 0;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Check if version satisfies a range expression
|
|
81
|
+
* Supports: <1.2.3, <=1.2.3, >1.2.3, >=1.2.3, 1.2.3 (exact)
|
|
82
|
+
*/
|
|
83
|
+
function satisfiesRange(version, range) {
|
|
84
|
+
const trimmed = range.trim();
|
|
85
|
+
if (trimmed.startsWith('<=')) {
|
|
86
|
+
return compareSemver(version, trimmed.slice(2)) <= 0;
|
|
87
|
+
}
|
|
88
|
+
if (trimmed.startsWith('<')) {
|
|
89
|
+
return compareSemver(version, trimmed.slice(1)) < 0;
|
|
90
|
+
}
|
|
91
|
+
if (trimmed.startsWith('>=')) {
|
|
92
|
+
return compareSemver(version, trimmed.slice(2)) >= 0;
|
|
93
|
+
}
|
|
94
|
+
if (trimmed.startsWith('>')) {
|
|
95
|
+
return compareSemver(version, trimmed.slice(1)) > 0;
|
|
96
|
+
}
|
|
97
|
+
// Exact match
|
|
98
|
+
return compareSemver(version, trimmed) === 0;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Check if version is affected by vulnerability
|
|
102
|
+
* affectedVersions format: "<4.17.21" or ">=1.0.0 <2.0.0"
|
|
103
|
+
*/
|
|
104
|
+
function isAffected(version, affectedVersions) {
|
|
105
|
+
// Split on spaces for compound ranges
|
|
106
|
+
const parts = affectedVersions.split(/\s+/).filter(Boolean);
|
|
107
|
+
// All conditions must be satisfied
|
|
108
|
+
return parts.every(part => satisfiesRange(version, part));
|
|
109
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "guardrail-security",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
|
+
"description": "Guardrail Security - Secret detection and vulnerability scanning",
|
|
4
5
|
"main": "./dist/index.js",
|
|
5
|
-
"files": ["dist/**/*"
|
|
6
|
+
"files": ["dist/**/*"],
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"author": "Guardrail Team <team@getguardrail.io>",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/guardiavault-oss/codeguard.git"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
6
16
|
"types": "./dist/index.d.ts",
|
|
7
17
|
"exports": {
|
|
8
18
|
".": {
|
|
@@ -22,11 +32,15 @@
|
|
|
22
32
|
"@azure/identity": "^4.0.0",
|
|
23
33
|
"@google-cloud/secret-manager": "^5.0.0",
|
|
24
34
|
"node-vault": "^0.10.2",
|
|
25
|
-
"glob": "^10.3.10"
|
|
35
|
+
"glob": "^10.3.10",
|
|
36
|
+
"yaml": "^2.3.4"
|
|
26
37
|
},
|
|
27
38
|
"devDependencies": {
|
|
28
39
|
"@types/node": "^20.10.0",
|
|
29
40
|
"typescript": "^5.3.3",
|
|
30
41
|
"vitest": "^1.2.0"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=18.0.0"
|
|
31
45
|
}
|
|
32
46
|
}
|
|
@@ -1,250 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* License Compliance Engine Tests
|
|
3
|
-
*
|
|
4
|
-
* Test suite for the LicenseComplianceEngine class
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, beforeEach, jest } from "@jest/globals";
|
|
8
|
-
import { LicenseComplianceEngine } from "../../license/engine";
|
|
9
|
-
|
|
10
|
-
// Import ResponseType
|
|
11
|
-
type ResponseType =
|
|
12
|
-
| "basic"
|
|
13
|
-
| "cors"
|
|
14
|
-
| "default"
|
|
15
|
-
| "error"
|
|
16
|
-
| "opaque"
|
|
17
|
-
| "opaqueredirect";
|
|
18
|
-
|
|
19
|
-
// Mock fs
|
|
20
|
-
jest.mock("fs", () => ({
|
|
21
|
-
readFileSync: jest.fn<(path: string) => string>().mockReturnValue(""),
|
|
22
|
-
existsSync: jest.fn<(path: string) => boolean>(() => true),
|
|
23
|
-
}));
|
|
24
|
-
|
|
25
|
-
// Mock prisma
|
|
26
|
-
jest.mock("@guardrail/database", () => ({
|
|
27
|
-
prisma: {
|
|
28
|
-
licenseAnalysis: {
|
|
29
|
-
findMany: jest.fn<() => Promise<any[]>>().mockResolvedValue([]),
|
|
30
|
-
create: jest.fn<() => Promise<any>>().mockResolvedValue({}),
|
|
31
|
-
update: jest.fn<() => Promise<any>>().mockResolvedValue({}),
|
|
32
|
-
delete: jest.fn<() => Promise<any>>().mockResolvedValue({}),
|
|
33
|
-
findUnique: jest.fn<() => Promise<any>>().mockResolvedValue(null),
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
}));
|
|
37
|
-
|
|
38
|
-
// Mock fetch
|
|
39
|
-
const mockFetch = jest.fn<() => Promise<Response>>();
|
|
40
|
-
global.fetch = mockFetch;
|
|
41
|
-
|
|
42
|
-
describe("LicenseComplianceEngine", () => {
|
|
43
|
-
let engine: LicenseComplianceEngine;
|
|
44
|
-
|
|
45
|
-
beforeEach(() => {
|
|
46
|
-
engine = new LicenseComplianceEngine();
|
|
47
|
-
jest.clearAllMocks();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
describe("license fetching", () => {
|
|
51
|
-
it("should fetch license from npm registry", async () => {
|
|
52
|
-
const mockResponse = {
|
|
53
|
-
ok: true,
|
|
54
|
-
json: jest.fn<() => Promise<any>>().mockResolvedValue({
|
|
55
|
-
"dist-tags": { latest: "1.0.0" },
|
|
56
|
-
versions: {
|
|
57
|
-
"1.0.0": {
|
|
58
|
-
license: "MIT",
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
}),
|
|
62
|
-
headers: new Headers(),
|
|
63
|
-
status: 200,
|
|
64
|
-
statusText: "OK",
|
|
65
|
-
type: "basic" as ResponseType,
|
|
66
|
-
url: "",
|
|
67
|
-
redirected: false,
|
|
68
|
-
clone: jest.fn<() => Response>(),
|
|
69
|
-
body: null,
|
|
70
|
-
bodyUsed: false,
|
|
71
|
-
bytes: jest.fn<() => Promise<Uint8Array<ArrayBufferLike>>>(),
|
|
72
|
-
text: jest.fn<() => Promise<string>>(),
|
|
73
|
-
blob: jest.fn<() => Promise<Blob>>(),
|
|
74
|
-
formData: jest.fn<() => Promise<FormData>>(),
|
|
75
|
-
arrayBuffer: jest.fn<() => Promise<ArrayBuffer>>(),
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
mockFetch.mockResolvedValue(mockResponse);
|
|
79
|
-
|
|
80
|
-
// Access private method through type assertion
|
|
81
|
-
const result = await (engine as any).fetchLicenseFromRegistry(
|
|
82
|
-
"test-package",
|
|
83
|
-
);
|
|
84
|
-
|
|
85
|
-
expect(result.license).toBe("MIT");
|
|
86
|
-
expect(result.category).toBe("permissive");
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("should handle SPDX expressions", async () => {
|
|
90
|
-
const mockResponse = {
|
|
91
|
-
ok: true,
|
|
92
|
-
json: jest.fn<() => Promise<any>>().mockResolvedValue({
|
|
93
|
-
"dist-tags": { latest: "1.0.0" },
|
|
94
|
-
versions: {
|
|
95
|
-
"1.0.0": {
|
|
96
|
-
license: {
|
|
97
|
-
type: "MIT",
|
|
98
|
-
},
|
|
99
|
-
},
|
|
100
|
-
},
|
|
101
|
-
}),
|
|
102
|
-
headers: new Headers(),
|
|
103
|
-
status: 200,
|
|
104
|
-
statusText: "OK",
|
|
105
|
-
type: "basic" as ResponseType,
|
|
106
|
-
url: "",
|
|
107
|
-
redirected: false,
|
|
108
|
-
clone: jest.fn<() => Response>(),
|
|
109
|
-
body: null,
|
|
110
|
-
bodyUsed: false,
|
|
111
|
-
bytes: jest.fn<() => Promise<Uint8Array<ArrayBufferLike>>>(),
|
|
112
|
-
text: jest.fn<() => Promise<string>>(),
|
|
113
|
-
blob: jest.fn<() => Promise<Blob>>(),
|
|
114
|
-
formData: jest.fn<() => Promise<FormData>>(),
|
|
115
|
-
arrayBuffer: jest.fn<() => Promise<ArrayBuffer>>(),
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
mockFetch.mockResolvedValue(mockResponse);
|
|
119
|
-
|
|
120
|
-
const result = await (engine as any).fetchLicenseFromRegistry(
|
|
121
|
-
"test-package",
|
|
122
|
-
);
|
|
123
|
-
|
|
124
|
-
expect(result.license).toBe("MIT");
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it("should normalize license names", async () => {
|
|
128
|
-
// Test the normalizeLicenseName method directly
|
|
129
|
-
const result = (engine as any).normalizeLicenseName("Apache 2.0");
|
|
130
|
-
expect(result).toBe("Apache-2.0");
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("should use fallback on network error", async () => {
|
|
134
|
-
mockFetch.mockRejectedValue(new Error("Network error"));
|
|
135
|
-
|
|
136
|
-
const result = await (engine as any).fetchLicenseFromRegistry(
|
|
137
|
-
"private-package",
|
|
138
|
-
);
|
|
139
|
-
|
|
140
|
-
expect(result.license).toBe("UNKNOWN");
|
|
141
|
-
expect(result.category).toBe("unknown");
|
|
142
|
-
});
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
describe("license categorization", () => {
|
|
146
|
-
it("should categorize MIT as permissive", () => {
|
|
147
|
-
const result = (engine as any).categorizeLicense("MIT");
|
|
148
|
-
expect(result).toBe("permissive");
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it("should categorize GPL as copyleft", () => {
|
|
152
|
-
const result = (engine as any).categorizeLicense("GPL-3.0");
|
|
153
|
-
expect(result).toBe("copyleft");
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("should categorize LGPL as weak-copyleft", () => {
|
|
157
|
-
const result = (engine as any).categorizeLicense("LGPL-3.0");
|
|
158
|
-
expect(result).toBe("weak-copyleft");
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it("should categorize unknown licenses", () => {
|
|
162
|
-
const result = (engine as any).categorizeLicense("UNKNOWN-LICENSE");
|
|
163
|
-
expect(result).toBe("unknown");
|
|
164
|
-
});
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
describe("conflict detection", () => {
|
|
168
|
-
it("should detect GPL contamination in proprietary project", () => {
|
|
169
|
-
const dependencies = [
|
|
170
|
-
{ name: "dep1", license: "MIT", category: "permissive" },
|
|
171
|
-
{ name: "dep2", license: "GPL-3.0", category: "copyleft" },
|
|
172
|
-
];
|
|
173
|
-
|
|
174
|
-
const conflicts = (engine as any).detectGPLContamination(
|
|
175
|
-
dependencies,
|
|
176
|
-
"Proprietary",
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
expect(conflicts).toHaveLength(2);
|
|
180
|
-
expect(conflicts[0].dependency).toBe("dep1");
|
|
181
|
-
expect(conflicts[0].severity).toBe("warning");
|
|
182
|
-
expect(conflicts[1].dependency).toBe("dep2");
|
|
183
|
-
expect(conflicts[1].severity).toBe("error");
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it("should allow LGPL in Apache project", () => {
|
|
187
|
-
const dependencies = [
|
|
188
|
-
{ name: "dep1", license: "Apache-2.0", category: "permissive" },
|
|
189
|
-
{ name: "dep2", license: "LGPL-3.0", category: "weak-copyleft" },
|
|
190
|
-
];
|
|
191
|
-
|
|
192
|
-
const conflicts = (engine as any).detectGPLContamination(
|
|
193
|
-
dependencies,
|
|
194
|
-
"Apache-2.0",
|
|
195
|
-
);
|
|
196
|
-
|
|
197
|
-
expect(conflicts).toHaveLength(0);
|
|
198
|
-
});
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
describe("project analysis", () => {
|
|
202
|
-
it("should analyze simple project", async () => {
|
|
203
|
-
const mockFs = await import("fs");
|
|
204
|
-
jest.mocked(mockFs.readFileSync).mockReturnValue(
|
|
205
|
-
JSON.stringify({
|
|
206
|
-
dependencies: {
|
|
207
|
-
express: "^4.18.0",
|
|
208
|
-
lodash: "^4.17.21",
|
|
209
|
-
},
|
|
210
|
-
}),
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
// Mock fetch responses
|
|
214
|
-
mockFetch.mockResolvedValue({
|
|
215
|
-
ok: true,
|
|
216
|
-
json: jest.fn<() => Promise<any>>().mockResolvedValue({
|
|
217
|
-
"dist-tags": { latest: "1.0.0" },
|
|
218
|
-
versions: {
|
|
219
|
-
"1.0.0": { license: "MIT" },
|
|
220
|
-
},
|
|
221
|
-
}),
|
|
222
|
-
headers: new Headers(),
|
|
223
|
-
status: 200,
|
|
224
|
-
statusText: "OK",
|
|
225
|
-
type: "basic" as ResponseType,
|
|
226
|
-
url: "",
|
|
227
|
-
redirected: false,
|
|
228
|
-
clone: jest.fn<() => Response>(),
|
|
229
|
-
body: null,
|
|
230
|
-
bodyUsed: false,
|
|
231
|
-
bytes: jest.fn<() => Promise<Uint8Array<ArrayBufferLike>>>(),
|
|
232
|
-
text: jest.fn<() => Promise<string>>(),
|
|
233
|
-
blob: jest.fn<() => Promise<Blob>>(),
|
|
234
|
-
formData: jest.fn<() => Promise<FormData>>(),
|
|
235
|
-
arrayBuffer: jest.fn<() => Promise<ArrayBuffer>>(),
|
|
236
|
-
});
|
|
237
|
-
|
|
238
|
-
const result = await engine.analyzeProject(
|
|
239
|
-
"/test/path",
|
|
240
|
-
"test-project",
|
|
241
|
-
"MIT",
|
|
242
|
-
);
|
|
243
|
-
|
|
244
|
-
expect(result.projectId).toBe("test-project");
|
|
245
|
-
expect(result.projectLicense).toBe("MIT");
|
|
246
|
-
expect(result.dependencies).toHaveLength(2);
|
|
247
|
-
expect(result.overallStatus).toBe("compliant");
|
|
248
|
-
});
|
|
249
|
-
});
|
|
250
|
-
});
|
|
@@ -1,191 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Typosquat Detector Tests
|
|
3
|
-
*
|
|
4
|
-
* Test suite for the TyposquatDetector class
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { describe, it, expect, beforeEach } from "@jest/globals";
|
|
8
|
-
import { TyposquatDetector } from "../../supply-chain/typosquat";
|
|
9
|
-
|
|
10
|
-
describe("TyposquatDetector", () => {
|
|
11
|
-
let detector: TyposquatDetector;
|
|
12
|
-
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
detector = new TyposquatDetector();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
describe("Exact Match Detection", () => {
|
|
18
|
-
const legitimatePackages = [
|
|
19
|
-
"react",
|
|
20
|
-
"vue",
|
|
21
|
-
"express",
|
|
22
|
-
"lodash",
|
|
23
|
-
"typescript",
|
|
24
|
-
"axios",
|
|
25
|
-
"webpack",
|
|
26
|
-
"jest",
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
it.each(legitimatePackages)(
|
|
30
|
-
"should NOT flag legitimate package: %s",
|
|
31
|
-
async (pkg) => {
|
|
32
|
-
const result = await detector.detectTyposquatting(pkg);
|
|
33
|
-
expect(result.isTyposquat).toBe(false);
|
|
34
|
-
},
|
|
35
|
-
);
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe("Character Swap Detection", () => {
|
|
39
|
-
const swapAttempts = [
|
|
40
|
-
{ input: "raect", target: "react" },
|
|
41
|
-
{ input: "exrpess", target: "express" },
|
|
42
|
-
{ input: "loadsh", target: "lodash" },
|
|
43
|
-
{ input: "axois", target: "axios" },
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
it.each(swapAttempts)(
|
|
47
|
-
"should detect character swap: $input -> $target",
|
|
48
|
-
async ({ input, target }) => {
|
|
49
|
-
const result = await detector.detectTyposquatting(input);
|
|
50
|
-
expect(result.isTyposquat).toBe(true);
|
|
51
|
-
expect(result.targetPackage).toBe(target);
|
|
52
|
-
expect(result.patterns).toContain("character_swap");
|
|
53
|
-
},
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
describe("Missing Character Detection", () => {
|
|
58
|
-
const missingCharAttempts = [
|
|
59
|
-
{ input: "rect", target: "react" },
|
|
60
|
-
{ input: "expres", target: "express" },
|
|
61
|
-
{ input: "lodas", target: "lodash" },
|
|
62
|
-
{ input: "webpck", target: "webpack" },
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
it.each(missingCharAttempts)(
|
|
66
|
-
"should detect missing character: $input -> $target",
|
|
67
|
-
async ({ input, target }) => {
|
|
68
|
-
const result = await detector.detectTyposquatting(input);
|
|
69
|
-
expect(result.isTyposquat).toBe(true);
|
|
70
|
-
expect(result.targetPackage).toBe(target);
|
|
71
|
-
expect(result.patterns).toContain("missing_character");
|
|
72
|
-
},
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
describe("Extra Character Detection", () => {
|
|
77
|
-
const extraCharAttempts = [
|
|
78
|
-
{ input: "reactt", target: "react" },
|
|
79
|
-
{ input: "expresss", target: "express" },
|
|
80
|
-
{ input: "lodassh", target: "lodash" },
|
|
81
|
-
{ input: "axioss", target: "axios" },
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
it.each(extraCharAttempts)(
|
|
85
|
-
"should detect extra character: $input -> $target",
|
|
86
|
-
async ({ input, target }) => {
|
|
87
|
-
const result = await detector.detectTyposquatting(input);
|
|
88
|
-
expect(result.isTyposquat).toBe(true);
|
|
89
|
-
expect(result.targetPackage).toBe(target);
|
|
90
|
-
expect(result.patterns).toContain("extra_character");
|
|
91
|
-
},
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe("Homoglyph Detection", () => {
|
|
96
|
-
const homoglyphAttempts = [
|
|
97
|
-
{ input: "reаct", description: "cyrillic a" },
|
|
98
|
-
{ input: "exprеss", description: "cyrillic e" },
|
|
99
|
-
{ input: "l0dash", description: "zero for o" },
|
|
100
|
-
{ input: "ax1os", description: "one for i" },
|
|
101
|
-
];
|
|
102
|
-
|
|
103
|
-
it.each(homoglyphAttempts)(
|
|
104
|
-
"should detect homoglyph attack: $input ($description)",
|
|
105
|
-
async ({ input }) => {
|
|
106
|
-
const result = await detector.detectTyposquatting(input);
|
|
107
|
-
// Homoglyph detection may vary based on implementation
|
|
108
|
-
expect(result.similarity).toBeGreaterThan(0);
|
|
109
|
-
},
|
|
110
|
-
);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
describe("Combosquatting Detection", () => {
|
|
114
|
-
const comboAttempts = [
|
|
115
|
-
{ input: "react-native-helper", target: "react" },
|
|
116
|
-
{ input: "express-security", target: "express" },
|
|
117
|
-
{ input: "lodash-utils", target: "lodash" },
|
|
118
|
-
{ input: "axios-wrapper", target: "axios" },
|
|
119
|
-
];
|
|
120
|
-
|
|
121
|
-
it.each(comboAttempts)(
|
|
122
|
-
"should detect combosquatting: $input -> $target",
|
|
123
|
-
async ({ input }) => {
|
|
124
|
-
const result = await detector.detectTyposquatting(input);
|
|
125
|
-
if (result.isTyposquat) {
|
|
126
|
-
expect(result.patterns).toContain("combosquatting");
|
|
127
|
-
}
|
|
128
|
-
},
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe("Levenshtein Distance", () => {
|
|
133
|
-
it("should calculate similarity based on Levenshtein distance", async () => {
|
|
134
|
-
const result = await detector.detectTyposquatting("reakt");
|
|
135
|
-
expect(result.similarity).toBeGreaterThan(0.7);
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
it("should have low similarity for unrelated packages", async () => {
|
|
139
|
-
const result = await detector.detectTyposquatting(
|
|
140
|
-
"completely-different-package",
|
|
141
|
-
);
|
|
142
|
-
expect(result.similarity).toBeLessThan(0.5);
|
|
143
|
-
});
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
describe("Result Structure", () => {
|
|
147
|
-
it("should return complete result structure", async () => {
|
|
148
|
-
const result = await detector.detectTyposquatting("reakt");
|
|
149
|
-
|
|
150
|
-
expect(result).toHaveProperty("isTyposquat");
|
|
151
|
-
expect(result).toHaveProperty("suspiciousPackage");
|
|
152
|
-
expect(result).toHaveProperty("similarity");
|
|
153
|
-
expect(result).toHaveProperty("patterns");
|
|
154
|
-
expect(Array.isArray(result.patterns)).toBe(true);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it("should include target package when typosquat detected", async () => {
|
|
158
|
-
const result = await detector.detectTyposquatting("reakt");
|
|
159
|
-
|
|
160
|
-
if (result.isTyposquat) {
|
|
161
|
-
expect(result.targetPackage).toBeDefined();
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe("Edge Cases", () => {
|
|
167
|
-
it("should handle empty string", async () => {
|
|
168
|
-
const result = await detector.detectTyposquatting("");
|
|
169
|
-
expect(result.isTyposquat).toBe(false);
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
it("should handle very long package names", async () => {
|
|
173
|
-
const longName = "a".repeat(100);
|
|
174
|
-
const result = await detector.detectTyposquatting(longName);
|
|
175
|
-
expect(result).toBeDefined();
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
it("should handle special characters", async () => {
|
|
179
|
-
const result = await detector.detectTyposquatting("@scope/react");
|
|
180
|
-
expect(result).toBeDefined();
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
it("should be case insensitive", async () => {
|
|
184
|
-
const result1 = await detector.detectTyposquatting("REACT");
|
|
185
|
-
const result2 = await detector.detectTyposquatting("React");
|
|
186
|
-
// Both should handle case variations
|
|
187
|
-
expect(result1).toBeDefined();
|
|
188
|
-
expect(result2).toBeDefined();
|
|
189
|
-
});
|
|
190
|
-
});
|
|
191
|
-
});
|