@havoc-security/scanner 0.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.
Files changed (140) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +22 -0
  3. package/dist/analyzers/AuthorizationCoverageAnalyzer.d.ts +7 -0
  4. package/dist/analyzers/AuthorizationCoverageAnalyzer.d.ts.map +1 -0
  5. package/dist/analyzers/AuthorizationCoverageAnalyzer.js +100 -0
  6. package/dist/analyzers/AuthorizationCoverageAnalyzer.js.map +1 -0
  7. package/dist/analyzers/CredentialExposureAnalyzer.d.ts +11 -0
  8. package/dist/analyzers/CredentialExposureAnalyzer.d.ts.map +1 -0
  9. package/dist/analyzers/CredentialExposureAnalyzer.js +262 -0
  10. package/dist/analyzers/CredentialExposureAnalyzer.js.map +1 -0
  11. package/dist/analyzers/DependencyAuditAnalyzer.d.ts +28 -0
  12. package/dist/analyzers/DependencyAuditAnalyzer.d.ts.map +1 -0
  13. package/dist/analyzers/DependencyAuditAnalyzer.js +107 -0
  14. package/dist/analyzers/DependencyAuditAnalyzer.js.map +1 -0
  15. package/dist/analyzers/EncryptionAnalyzer.d.ts +7 -0
  16. package/dist/analyzers/EncryptionAnalyzer.d.ts.map +1 -0
  17. package/dist/analyzers/EncryptionAnalyzer.js +170 -0
  18. package/dist/analyzers/EncryptionAnalyzer.js.map +1 -0
  19. package/dist/analyzers/FileUploadAnalyzer.d.ts +8 -0
  20. package/dist/analyzers/FileUploadAnalyzer.d.ts.map +1 -0
  21. package/dist/analyzers/FileUploadAnalyzer.js +193 -0
  22. package/dist/analyzers/FileUploadAnalyzer.js.map +1 -0
  23. package/dist/analyzers/IdorAnalyzer.d.ts +7 -0
  24. package/dist/analyzers/IdorAnalyzer.d.ts.map +1 -0
  25. package/dist/analyzers/IdorAnalyzer.js +91 -0
  26. package/dist/analyzers/IdorAnalyzer.js.map +1 -0
  27. package/dist/analyzers/MassAssignmentAnalyzer.d.ts +7 -0
  28. package/dist/analyzers/MassAssignmentAnalyzer.d.ts.map +1 -0
  29. package/dist/analyzers/MassAssignmentAnalyzer.js +90 -0
  30. package/dist/analyzers/MassAssignmentAnalyzer.js.map +1 -0
  31. package/dist/analyzers/PrivilegeEscalationAnalyzer.d.ts +7 -0
  32. package/dist/analyzers/PrivilegeEscalationAnalyzer.d.ts.map +1 -0
  33. package/dist/analyzers/PrivilegeEscalationAnalyzer.js +217 -0
  34. package/dist/analyzers/PrivilegeEscalationAnalyzer.js.map +1 -0
  35. package/dist/analyzers/RateLimitAnalyzer.d.ts +7 -0
  36. package/dist/analyzers/RateLimitAnalyzer.d.ts.map +1 -0
  37. package/dist/analyzers/RateLimitAnalyzer.js +151 -0
  38. package/dist/analyzers/RateLimitAnalyzer.js.map +1 -0
  39. package/dist/analyzers/SessionSecurityAnalyzer.d.ts +10 -0
  40. package/dist/analyzers/SessionSecurityAnalyzer.d.ts.map +1 -0
  41. package/dist/analyzers/SessionSecurityAnalyzer.js +295 -0
  42. package/dist/analyzers/SessionSecurityAnalyzer.js.map +1 -0
  43. package/dist/analyzers/SqlInjectionAnalyzer.d.ts +7 -0
  44. package/dist/analyzers/SqlInjectionAnalyzer.d.ts.map +1 -0
  45. package/dist/analyzers/SqlInjectionAnalyzer.js +77 -0
  46. package/dist/analyzers/SqlInjectionAnalyzer.js.map +1 -0
  47. package/dist/analyzers/XssSurfaceAnalyzer.d.ts +7 -0
  48. package/dist/analyzers/XssSurfaceAnalyzer.d.ts.map +1 -0
  49. package/dist/analyzers/XssSurfaceAnalyzer.js +100 -0
  50. package/dist/analyzers/XssSurfaceAnalyzer.js.map +1 -0
  51. package/dist/analyzers/index.d.ts +13 -0
  52. package/dist/analyzers/index.d.ts.map +1 -0
  53. package/dist/analyzers/index.js +13 -0
  54. package/dist/analyzers/index.js.map +1 -0
  55. package/dist/index.d.ts +17 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +139 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/parsers/PhpParser.d.ts +56 -0
  60. package/dist/parsers/PhpParser.d.ts.map +1 -0
  61. package/dist/parsers/PhpParser.js +193 -0
  62. package/dist/parsers/PhpParser.js.map +1 -0
  63. package/dist/parsers/RouteParser.d.ts +87 -0
  64. package/dist/parsers/RouteParser.d.ts.map +1 -0
  65. package/dist/parsers/RouteParser.js +327 -0
  66. package/dist/parsers/RouteParser.js.map +1 -0
  67. package/dist/rules/index.d.ts +14 -0
  68. package/dist/rules/index.d.ts.map +1 -0
  69. package/dist/rules/index.js +9 -0
  70. package/dist/rules/index.js.map +1 -0
  71. package/dist/types/index.d.ts +137 -0
  72. package/dist/types/index.d.ts.map +1 -0
  73. package/dist/types/index.js +13 -0
  74. package/dist/types/index.js.map +1 -0
  75. package/package.json +30 -0
  76. package/package.json.bak +27 -0
  77. package/src/analyzers/AuthorizationCoverageAnalyzer.ts +213 -0
  78. package/src/analyzers/CredentialExposureAnalyzer.ts +312 -0
  79. package/src/analyzers/DependencyAuditAnalyzer.ts +135 -0
  80. package/src/analyzers/EncryptionAnalyzer.ts +195 -0
  81. package/src/analyzers/FileUploadAnalyzer.ts +239 -0
  82. package/src/analyzers/IdorAnalyzer.ts +118 -0
  83. package/src/analyzers/InsecureDeserializationAnalyzer.ts +212 -0
  84. package/src/analyzers/MassAssignmentAnalyzer.ts +105 -0
  85. package/src/analyzers/OpenRedirectAnalyzer.ts +149 -0
  86. package/src/analyzers/PrivilegeEscalationAnalyzer.ts +258 -0
  87. package/src/analyzers/RateLimitAnalyzer.ts +195 -0
  88. package/src/analyzers/SecurityHeaderAnalyzer.ts +263 -0
  89. package/src/analyzers/SessionSecurityAnalyzer.ts +342 -0
  90. package/src/analyzers/SqlInjectionAnalyzer.ts +99 -0
  91. package/src/analyzers/XssSurfaceAnalyzer.ts +112 -0
  92. package/src/analyzers/exclusions.ts +87 -0
  93. package/src/analyzers/index.ts +15 -0
  94. package/src/index.ts +226 -0
  95. package/src/parsers/PhpParser.ts +259 -0
  96. package/src/parsers/RouteParser.ts +384 -0
  97. package/src/rules/index.ts +16 -0
  98. package/src/types/index.ts +164 -0
  99. package/tests/EncryptionAnalyzer.test.ts +137 -0
  100. package/tests/PrivilegeEscalationAnalyzer.test.ts +141 -0
  101. package/tests/RateLimitAnalyzer.test.ts +112 -0
  102. package/tests/analyzers.test.ts +678 -0
  103. package/tests/auth-coverage-route-aware.test.ts +294 -0
  104. package/tests/credential-exposure.test.ts +142 -0
  105. package/tests/file-upload.test.ts +141 -0
  106. package/tests/fixtures/app/Http/Controllers/AdminController.php +19 -0
  107. package/tests/fixtures/app/Http/Controllers/PostController.php +49 -0
  108. package/tests/fixtures/app/Http/Controllers/PublicController.php +17 -0
  109. package/tests/fixtures/app/Models/Comment.php +11 -0
  110. package/tests/fixtures/app/Models/OpenModel.php +11 -0
  111. package/tests/fixtures/app/Models/Post.php +14 -0
  112. package/tests/fixtures/app/Models/SafeModel.php +10 -0
  113. package/tests/fixtures/app/Models/User.php +15 -0
  114. package/tests/fixtures/blade/mail.blade.php +8 -0
  115. package/tests/fixtures/blade/safe.blade.php +12 -0
  116. package/tests/fixtures/blade/vulnerable.blade.php +12 -0
  117. package/tests/fixtures/controllers/AdminController.php +19 -0
  118. package/tests/fixtures/controllers/PostController.php +49 -0
  119. package/tests/fixtures/controllers/PublicController.php +17 -0
  120. package/tests/fixtures/deserialization/safe.php +32 -0
  121. package/tests/fixtures/deserialization/unsafe.php +60 -0
  122. package/tests/fixtures/models/Comment.php +11 -0
  123. package/tests/fixtures/models/OpenModel.php +11 -0
  124. package/tests/fixtures/models/Post.php +14 -0
  125. package/tests/fixtures/models/SafeModel.php +10 -0
  126. package/tests/fixtures/models/User.php +15 -0
  127. package/tests/fixtures/redirect/safe.php +38 -0
  128. package/tests/fixtures/redirect/unsafe.php +39 -0
  129. package/tests/fixtures/routes/api.php +9 -0
  130. package/tests/fixtures/routes/web.php +18 -0
  131. package/tests/fixtures/security-headers/app/Http/Middleware/SecurityHeaders.php +24 -0
  132. package/tests/fixtures/security-headers/app/Providers/AppServiceProvider.php +16 -0
  133. package/tests/fixtures/sql/safe_queries.php +7 -0
  134. package/tests/fixtures/sql/vulnerable_queries.php +7 -0
  135. package/tests/new-analyzers.test.ts +373 -0
  136. package/tests/route-parser.test.ts +257 -0
  137. package/tests/scanner.test.ts +82 -0
  138. package/tests/session-security.test.ts +161 -0
  139. package/tests/types.test.ts +29 -0
  140. package/tsconfig.json +9 -0
@@ -0,0 +1,82 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { scan, DEFAULT_CONFIG, Severity } from '../src/index.js';
3
+ import type { ParsedFile } from '../src/types/index.js';
4
+
5
+ describe('scan() orchestrator', () => {
6
+ it('returns a valid ScanResult shape for empty input', async () => {
7
+ const result = await scan([], DEFAULT_CONFIG);
8
+ expect(result).toHaveProperty('findings');
9
+ expect(result).toHaveProperty('coverage');
10
+ expect(result).toHaveProperty('metadata');
11
+ expect(Array.isArray(result.findings)).toBe(true);
12
+ });
13
+
14
+ it('metadata contains required fields', async () => {
15
+ const result = await scan([], DEFAULT_CONFIG);
16
+ expect(result.metadata.scannedAt).toBeTruthy();
17
+ expect(result.metadata.framework).toBe('laravel');
18
+ expect(result.metadata.filesScanned).toBe(0);
19
+ expect(typeof result.metadata.durationMs).toBe('number');
20
+ expect(result.metadata.scannerVersion).toBeTruthy();
21
+ });
22
+
23
+ it('counts files scanned correctly', async () => {
24
+ const files: ParsedFile[] = [
25
+ { path: 'a.php', content: '', ast: null },
26
+ { path: 'b.php', content: '', ast: null },
27
+ ];
28
+ const result = await scan(files, DEFAULT_CONFIG);
29
+ expect(result.metadata.filesScanned).toBe(2);
30
+ });
31
+
32
+ it('runs with custom analyzers', async () => {
33
+ const mockAnalyzer = {
34
+ name: 'MockAnalyzer',
35
+ description: 'Test',
36
+ analyze: async () => [{
37
+ id: 'MOCK-001', severity: Severity.Low, analyzer: 'MockAnalyzer',
38
+ title: 'Mock', description: 'Test', file: 'test.php', line: 1, recommendation: 'Fix it',
39
+ }],
40
+ };
41
+ const result = await scan([], DEFAULT_CONFIG, [mockAnalyzer]);
42
+ expect(result.findings).toHaveLength(1);
43
+ expect(result.findings[0].id).toBe('MOCK-001');
44
+ });
45
+
46
+ it('aggregates findings from multiple analyzers', async () => {
47
+ const a1 = {
48
+ name: 'A1', description: 'First',
49
+ analyze: async () => [{ id: 'A1-001', severity: Severity.High, analyzer: 'A1', title: 'A1', description: '', file: 'f.php', line: 1, recommendation: '' }],
50
+ };
51
+ const a2 = {
52
+ name: 'A2', description: 'Second',
53
+ analyze: async () => [{ id: 'A2-001', severity: Severity.Medium, analyzer: 'A2', title: 'A2', description: '', file: 'g.php', line: 5, recommendation: '' }],
54
+ };
55
+ const result = await scan([], DEFAULT_CONFIG, [a1, a2]);
56
+ expect(result.findings).toHaveLength(2);
57
+ });
58
+
59
+ it('coverage struct has all required fields', async () => {
60
+ const result = await scan([], DEFAULT_CONFIG);
61
+ expect(result.coverage).toHaveProperty('totalRoutes');
62
+ expect(result.coverage).toHaveProperty('coveredRoutes');
63
+ expect(result.coverage).toHaveProperty('coveragePercent');
64
+ expect(result.coverage).toHaveProperty('totalModels');
65
+ expect(result.coverage).toHaveProperty('protectedModels');
66
+ });
67
+
68
+ it('durationMs is a non-negative number', async () => {
69
+ const result = await scan([], DEFAULT_CONFIG);
70
+ expect(result.metadata.durationMs).toBeGreaterThanOrEqual(0);
71
+ });
72
+
73
+ it('scanner returns empty findings for empty analyzers list', async () => {
74
+ const result = await scan([], DEFAULT_CONFIG, []);
75
+ expect(result.findings).toHaveLength(0);
76
+ });
77
+
78
+ it('scanner version is a valid semver-like string', async () => {
79
+ const result = await scan([], DEFAULT_CONFIG);
80
+ expect(result.metadata.scannerVersion).toMatch(/^\d+\.\d+\.\d+$/);
81
+ });
82
+ });
@@ -0,0 +1,161 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { SessionSecurityAnalyzer } from '../src/analyzers/SessionSecurityAnalyzer.js';
3
+ import { DEFAULT_CONFIG } from '../src/index.js';
4
+ import type { ParsedFile } from '../src/types/index.js';
5
+ import { Severity } from '../src/types/index.js';
6
+
7
+ function makeFile(path: string, content: string): ParsedFile {
8
+ return { path, content, ast: null };
9
+ }
10
+
11
+ const analyzer = new SessionSecurityAnalyzer();
12
+
13
+ const SECURE_SESSION_CONFIG = `<?php
14
+ return [
15
+ 'driver' => env('SESSION_DRIVER', 'redis'),
16
+ 'lifetime' => env('SESSION_LIFETIME', 120),
17
+ 'secure' => env('SESSION_SECURE_COOKIE', true),
18
+ 'http_only' => true,
19
+ 'same_site' => 'lax',
20
+ ];`;
21
+
22
+ describe('SessionSecurityAnalyzer', () => {
23
+ it('has correct name', () => {
24
+ expect(analyzer.name).toBe('SessionSecurityAnalyzer');
25
+ });
26
+
27
+ it('returns empty for empty file list', async () => {
28
+ expect(await analyzer.analyze([], DEFAULT_CONFIG)).toHaveLength(0);
29
+ });
30
+
31
+ // Secure flag
32
+ it("flags 'secure' => false as HIGH", async () => {
33
+ const file = makeFile('config/session.php', `<?php
34
+ return [
35
+ 'secure' => false,
36
+ 'http_only' => true,
37
+ 'same_site' => 'lax',
38
+ 'driver' => 'redis',
39
+ 'lifetime' => 120,
40
+ ];`);
41
+ const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
42
+ expect(findings.some(f => f.id === 'HAVOC-SESS-001' && f.severity === Severity.High)).toBe(true);
43
+ });
44
+
45
+ it('does NOT flag secure => env(...) with no explicit false', async () => {
46
+ const file = makeFile('config/session.php', SECURE_SESSION_CONFIG);
47
+ const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
48
+ expect(findings.filter(f => f.id === 'HAVOC-SESS-001')).toHaveLength(0);
49
+ });
50
+
51
+ // http_only flag
52
+ it("flags 'http_only' => false as HIGH", async () => {
53
+ const file = makeFile('config/session.php', `<?php
54
+ return [
55
+ 'http_only' => false,
56
+ 'secure' => true,
57
+ 'same_site' => 'lax',
58
+ 'driver' => 'redis',
59
+ 'lifetime' => 120,
60
+ ];`);
61
+ const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
62
+ expect(findings.some(f => f.id === 'HAVOC-SESS-002' && f.severity === Severity.High)).toBe(true);
63
+ });
64
+
65
+ // same_site
66
+ it("flags same_site => 'none' as MEDIUM", async () => {
67
+ const file = makeFile('config/session.php', `<?php
68
+ return [
69
+ 'same_site' => 'none',
70
+ 'secure' => true,
71
+ 'http_only' => true,
72
+ 'driver' => 'redis',
73
+ 'lifetime' => 120,
74
+ ];`);
75
+ const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
76
+ expect(findings.some(f => f.id === 'HAVOC-SESS-003' && f.severity === Severity.Medium)).toBe(true);
77
+ });
78
+
79
+ it("does NOT flag same_site => 'lax'", async () => {
80
+ const file = makeFile('config/session.php', SECURE_SESSION_CONFIG);
81
+ const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
82
+ expect(findings.filter(f => f.id === 'HAVOC-SESS-003')).toHaveLength(0);
83
+ });
84
+
85
+ // File driver
86
+ it("flags driver => 'file' as MEDIUM", async () => {
87
+ const file = makeFile('config/session.php', `<?php
88
+ return [
89
+ 'driver' => 'file',
90
+ 'secure' => true,
91
+ 'http_only' => true,
92
+ 'same_site' => 'lax',
93
+ 'lifetime' => 120,
94
+ ];`);
95
+ const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
96
+ expect(findings.some(f => f.id === 'HAVOC-SESS-004' && f.severity === Severity.Medium)).toBe(true);
97
+ });
98
+
99
+ // Session lifetime
100
+ it('flags lifetime > 480 minutes as MEDIUM', async () => {
101
+ const file = makeFile('config/session.php', `<?php
102
+ return [
103
+ 'lifetime' => 1440,
104
+ 'driver' => 'redis',
105
+ 'secure' => true,
106
+ 'http_only' => true,
107
+ 'same_site' => 'lax',
108
+ ];`);
109
+ const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
110
+ expect(findings.some(f => f.id === 'HAVOC-SESS-006' && f.severity === Severity.Medium)).toBe(true);
111
+ });
112
+
113
+ it('does NOT flag lifetime <= 480 minutes', async () => {
114
+ const file = makeFile('config/session.php', SECURE_SESSION_CONFIG);
115
+ const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
116
+ expect(findings.filter(f => f.id === 'HAVOC-SESS-006')).toHaveLength(0);
117
+ });
118
+
119
+ // CSRF exclusions
120
+ it('flags non-empty $except in VerifyCsrfToken as HIGH', async () => {
121
+ const file = makeFile('app/Http/Middleware/VerifyCsrfToken.php', `<?php
122
+ class VerifyCsrfToken extends Middleware {
123
+ protected $except = [
124
+ 'stripe/webhook',
125
+ 'paypal/ipn',
126
+ ];
127
+ }`);
128
+ const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
129
+ expect(findings.some(f => f.id === 'HAVOC-SESS-005' && f.severity === Severity.High)).toBe(true);
130
+ });
131
+
132
+ it('does NOT flag empty $except array', async () => {
133
+ const file = makeFile('app/Http/Middleware/VerifyCsrfToken.php', `<?php
134
+ class VerifyCsrfToken extends Middleware {
135
+ protected $except = [];
136
+ }`);
137
+ const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
138
+ expect(findings.filter(f => f.id === 'HAVOC-SESS-005')).toHaveLength(0);
139
+ });
140
+
141
+ // Cookie encryption
142
+ it('flags non-empty $except in EncryptCookies', async () => {
143
+ const file = makeFile('app/Http/Middleware/EncryptCookies.php', `<?php
144
+ class EncryptCookies extends Middleware {
145
+ protected $except = [
146
+ 'analytics_id',
147
+ ];
148
+ }`);
149
+ const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
150
+ expect(findings.some(f => f.id === 'HAVOC-SESS-007')).toBe(true);
151
+ expect(findings.some(f => f.id === 'HAVOC-SESS-005')).toBe(false);
152
+ });
153
+
154
+ // Clean session config — no findings
155
+ it('produces no findings for a well-configured session config', async () => {
156
+ const file = makeFile('config/session.php', SECURE_SESSION_CONFIG);
157
+ const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
158
+ // Should have no findings on a clean config
159
+ expect(findings).toHaveLength(0);
160
+ });
161
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { Severity } from '../src/types/index.js';
3
+
4
+ describe('Severity enum', () => {
5
+ it('has Critical value', () => {
6
+ expect(Severity.Critical).toBe('critical');
7
+ });
8
+
9
+ it('has High value', () => {
10
+ expect(Severity.High).toBe('high');
11
+ });
12
+
13
+ it('has Medium value', () => {
14
+ expect(Severity.Medium).toBe('medium');
15
+ });
16
+
17
+ it('has Low value', () => {
18
+ expect(Severity.Low).toBe('low');
19
+ });
20
+
21
+ it('has Info value', () => {
22
+ expect(Severity.Info).toBe('info');
23
+ });
24
+
25
+ it('has exactly 5 severity levels', () => {
26
+ const values = Object.values(Severity);
27
+ expect(values).toHaveLength(5);
28
+ });
29
+ });
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "outDir": "./dist",
5
+ "rootDir": "./src"
6
+ },
7
+ "include": ["src/**/*.ts"],
8
+ "exclude": ["dist", "node_modules", "tests"]
9
+ }