guardrail-security 1.0.1 → 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 (66) hide show
  1. package/dist/attack-surface/analyzer.d.ts.map +1 -1
  2. package/dist/attack-surface/analyzer.js +3 -2
  3. package/dist/license/engine.d.ts.map +1 -1
  4. package/dist/license/engine.js +3 -2
  5. package/dist/sbom/generator.d.ts +42 -0
  6. package/dist/sbom/generator.d.ts.map +1 -1
  7. package/dist/sbom/generator.js +168 -7
  8. package/dist/secrets/allowlist.d.ts +38 -0
  9. package/dist/secrets/allowlist.d.ts.map +1 -0
  10. package/dist/secrets/allowlist.js +131 -0
  11. package/dist/secrets/config-loader.d.ts +25 -0
  12. package/dist/secrets/config-loader.d.ts.map +1 -0
  13. package/dist/secrets/config-loader.js +103 -0
  14. package/dist/secrets/contextual-risk.d.ts +19 -0
  15. package/dist/secrets/contextual-risk.d.ts.map +1 -0
  16. package/dist/secrets/contextual-risk.js +88 -0
  17. package/dist/secrets/git-scanner.d.ts +29 -0
  18. package/dist/secrets/git-scanner.d.ts.map +1 -0
  19. package/dist/secrets/git-scanner.js +109 -0
  20. package/dist/secrets/guardian.d.ts +70 -57
  21. package/dist/secrets/guardian.d.ts.map +1 -1
  22. package/dist/secrets/guardian.js +532 -240
  23. package/dist/secrets/index.d.ts +4 -0
  24. package/dist/secrets/index.d.ts.map +1 -1
  25. package/dist/secrets/index.js +11 -1
  26. package/dist/secrets/patterns.d.ts +39 -10
  27. package/dist/secrets/patterns.d.ts.map +1 -1
  28. package/dist/secrets/patterns.js +129 -71
  29. package/dist/secrets/pre-commit.d.ts.map +1 -1
  30. package/dist/secrets/pre-commit.js +1 -1
  31. package/dist/secrets/vault-integration.d.ts.map +1 -1
  32. package/dist/secrets/vault-integration.js +1 -0
  33. package/dist/supply-chain/detector.d.ts.map +1 -1
  34. package/dist/supply-chain/detector.js +4 -3
  35. package/dist/supply-chain/vulnerability-db.d.ts +89 -16
  36. package/dist/supply-chain/vulnerability-db.d.ts.map +1 -1
  37. package/dist/supply-chain/vulnerability-db.js +404 -115
  38. package/dist/utils/semver.d.ts +37 -0
  39. package/dist/utils/semver.d.ts.map +1 -0
  40. package/dist/utils/semver.js +109 -0
  41. package/package.json +17 -4
  42. package/src/__tests__/license/engine.test.ts +0 -250
  43. package/src/__tests__/supply-chain/typosquat.test.ts +0 -191
  44. package/src/attack-surface/analyzer.ts +0 -152
  45. package/src/attack-surface/index.ts +0 -5
  46. package/src/index.ts +0 -21
  47. package/src/languages/index.ts +0 -91
  48. package/src/languages/java-analyzer.ts +0 -490
  49. package/src/languages/python-analyzer.ts +0 -498
  50. package/src/license/compatibility-matrix.ts +0 -366
  51. package/src/license/engine.ts +0 -345
  52. package/src/license/index.ts +0 -6
  53. package/src/sbom/generator.ts +0 -355
  54. package/src/sbom/index.ts +0 -5
  55. package/src/secrets/guardian.ts +0 -448
  56. package/src/secrets/index.ts +0 -10
  57. package/src/secrets/patterns.ts +0 -186
  58. package/src/secrets/pre-commit.ts +0 -158
  59. package/src/secrets/vault-integration.ts +0 -360
  60. package/src/secrets/vault-providers.ts +0 -446
  61. package/src/supply-chain/detector.ts +0 -252
  62. package/src/supply-chain/index.ts +0 -11
  63. package/src/supply-chain/malicious-db.ts +0 -103
  64. package/src/supply-chain/script-analyzer.ts +0 -194
  65. package/src/supply-chain/typosquat.ts +0 -302
  66. 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.1",
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
  ".": {
@@ -17,17 +27,20 @@
17
27
  "clean": "rm -rf dist"
18
28
  },
19
29
  "dependencies": {
20
- "guardrail-core": "^1.0.0",
21
30
  "@aws-sdk/client-secrets-manager": "^3.490.0",
22
31
  "@azure/keyvault-secrets": "^4.8.0",
23
32
  "@azure/identity": "^4.0.0",
24
33
  "@google-cloud/secret-manager": "^5.0.0",
25
34
  "node-vault": "^0.10.2",
26
- "glob": "^10.3.10"
35
+ "glob": "^10.3.10",
36
+ "yaml": "^2.3.4"
27
37
  },
28
38
  "devDependencies": {
29
39
  "@types/node": "^20.10.0",
30
40
  "typescript": "^5.3.3",
31
41
  "vitest": "^1.2.0"
42
+ },
43
+ "engines": {
44
+ "node": ">=18.0.0"
32
45
  }
33
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
- });