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.
Files changed (60) hide show
  1. package/dist/sbom/generator.d.ts +42 -0
  2. package/dist/sbom/generator.d.ts.map +1 -1
  3. package/dist/sbom/generator.js +168 -7
  4. package/dist/secrets/allowlist.d.ts +38 -0
  5. package/dist/secrets/allowlist.d.ts.map +1 -0
  6. package/dist/secrets/allowlist.js +131 -0
  7. package/dist/secrets/config-loader.d.ts +25 -0
  8. package/dist/secrets/config-loader.d.ts.map +1 -0
  9. package/dist/secrets/config-loader.js +103 -0
  10. package/dist/secrets/contextual-risk.d.ts +19 -0
  11. package/dist/secrets/contextual-risk.d.ts.map +1 -0
  12. package/dist/secrets/contextual-risk.js +88 -0
  13. package/dist/secrets/git-scanner.d.ts +29 -0
  14. package/dist/secrets/git-scanner.d.ts.map +1 -0
  15. package/dist/secrets/git-scanner.js +109 -0
  16. package/dist/secrets/guardian.d.ts +70 -57
  17. package/dist/secrets/guardian.d.ts.map +1 -1
  18. package/dist/secrets/guardian.js +531 -258
  19. package/dist/secrets/index.d.ts +4 -0
  20. package/dist/secrets/index.d.ts.map +1 -1
  21. package/dist/secrets/index.js +11 -1
  22. package/dist/secrets/patterns.d.ts +39 -10
  23. package/dist/secrets/patterns.d.ts.map +1 -1
  24. package/dist/secrets/patterns.js +129 -71
  25. package/dist/secrets/pre-commit.d.ts.map +1 -1
  26. package/dist/secrets/pre-commit.js +1 -1
  27. package/dist/secrets/vault-integration.d.ts.map +1 -1
  28. package/dist/secrets/vault-integration.js +1 -0
  29. package/dist/supply-chain/vulnerability-db.d.ts +89 -16
  30. package/dist/supply-chain/vulnerability-db.d.ts.map +1 -1
  31. package/dist/supply-chain/vulnerability-db.js +404 -115
  32. package/dist/utils/semver.d.ts +37 -0
  33. package/dist/utils/semver.d.ts.map +1 -0
  34. package/dist/utils/semver.js +109 -0
  35. package/package.json +17 -3
  36. package/src/__tests__/license/engine.test.ts +0 -250
  37. package/src/__tests__/supply-chain/typosquat.test.ts +0 -191
  38. package/src/attack-surface/analyzer.ts +0 -153
  39. package/src/attack-surface/index.ts +0 -5
  40. package/src/index.ts +0 -21
  41. package/src/languages/index.ts +0 -91
  42. package/src/languages/java-analyzer.ts +0 -490
  43. package/src/languages/python-analyzer.ts +0 -498
  44. package/src/license/compatibility-matrix.ts +0 -366
  45. package/src/license/engine.ts +0 -346
  46. package/src/license/index.ts +0 -6
  47. package/src/sbom/generator.ts +0 -355
  48. package/src/sbom/index.ts +0 -5
  49. package/src/secrets/guardian.ts +0 -468
  50. package/src/secrets/index.ts +0 -10
  51. package/src/secrets/patterns.ts +0 -186
  52. package/src/secrets/pre-commit.ts +0 -158
  53. package/src/secrets/vault-integration.ts +0 -360
  54. package/src/secrets/vault-providers.ts +0 -446
  55. package/src/supply-chain/detector.ts +0 -253
  56. package/src/supply-chain/index.ts +0 -11
  57. package/src/supply-chain/malicious-db.ts +0 -103
  58. package/src/supply-chain/script-analyzer.ts +0 -194
  59. package/src/supply-chain/typosquat.ts +0 -302
  60. 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": "1.0.2",
3
+ "version": "2.0.0",
4
+ "description": "Guardrail Security - Secret detection and vulnerability scanning",
4
5
  "main": "./dist/index.js",
5
- "files": ["dist/**/*", "src/**/*"],
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
- });