@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,99 @@
1
+ import { Analyzer, Finding, HavocConfig, ParsedFile, Severity } from '../types/index.js';
2
+
3
+ const RAW_SQL_METHODS = [
4
+ 'whereRaw', 'orWhereRaw', 'orderByRaw', 'selectRaw',
5
+ 'havingRaw', 'orHavingRaw', 'groupByRaw', 'fromRaw',
6
+ ] as const;
7
+
8
+ const DB_FACADE_METHODS = [
9
+ 'DB::raw', 'DB::statement', 'DB::select', 'DB::insert', 'DB::update', 'DB::delete',
10
+ ] as const;
11
+
12
+ // Matches: "str" . $var, "$var inside string", $var . 'str'
13
+ const INTERPOLATION_PATTERN =
14
+ /(?:['"]\s*\.\s*\$|\$\{[^}]+\}|"[^"]*\$[a-zA-Z_][a-zA-Z0-9_]*[^"]*"|\$[a-zA-Z_][a-zA-Z0-9_]*\s*\.\s*['"])/;
15
+
16
+ // Matches ? placeholders or :named (but NOT ::method calls)
17
+ const BINDING_PATTERN = /\[\s*\$|\?\s*[,\]]|(?<![:])\s*:\s*(?!:)[a-zA-Z_]\w*/;
18
+
19
+ const FINDING_INJECTION = 'HAVOC-SQL-001';
20
+ const FINDING_RAW_INFO = 'HAVOC-SQL-002';
21
+ const ANALYZER_NAME = 'SqlInjectionAnalyzer';
22
+
23
+ interface SqlMatch {
24
+ method: string;
25
+ line: number;
26
+ snippet: string;
27
+ hasInterpolation: boolean;
28
+ hasBinding: boolean;
29
+ }
30
+
31
+ function findRawSqlUsages(content: string): SqlMatch[] {
32
+ const lines = content.split('\n');
33
+ const matches: SqlMatch[] = [];
34
+
35
+ lines.forEach((lineText, idx) => {
36
+ const lineNum = idx + 1;
37
+ for (const method of [...RAW_SQL_METHODS, ...DB_FACADE_METHODS]) {
38
+ const escaped = method.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
39
+ const methodPattern = new RegExp(`${escaped}\\s*\\(`, 'g');
40
+ if (methodPattern.test(lineText)) {
41
+ const hasInterpolation = INTERPOLATION_PATTERN.test(lineText);
42
+ const hasBinding = BINDING_PATTERN.test(lineText);
43
+ matches.push({ method, line: lineNum, snippet: lineText.trim(), hasInterpolation, hasBinding });
44
+ }
45
+ }
46
+ });
47
+
48
+ return matches;
49
+ }
50
+
51
+ export class SqlInjectionAnalyzer implements Analyzer {
52
+ readonly name = ANALYZER_NAME;
53
+ readonly description = 'Finds raw SQL queries with potential injection vulnerabilities';
54
+
55
+ async analyze(files: ParsedFile[], _config: HavocConfig): Promise<Finding[]> {
56
+ const findings: Finding[] = [];
57
+
58
+ for (const file of files) {
59
+ if (!file.path.endsWith('.php') || file.path.endsWith('.blade.php')) continue;
60
+
61
+ const usages = findRawSqlUsages(file.content);
62
+
63
+ for (const usage of usages) {
64
+ if (usage.hasInterpolation && !usage.hasBinding) {
65
+ findings.push({
66
+ id: FINDING_INJECTION,
67
+ severity: Severity.High,
68
+ analyzer: ANALYZER_NAME,
69
+ title: `SQL injection risk in \`${usage.method}()\``,
70
+ description:
71
+ `\`${usage.method}()\` is called with a string containing variable interpolation ` +
72
+ `or concatenation without parameter bindings.`,
73
+ file: file.path,
74
+ line: usage.line,
75
+ recommendation:
76
+ `Use parameter bindings: \`${usage.method}('col = ?', [$value])\`.`,
77
+ cwe: 'CWE-89',
78
+ snippet: usage.snippet,
79
+ });
80
+ } else if (!usage.hasBinding) {
81
+ findings.push({
82
+ id: FINDING_RAW_INFO,
83
+ severity: Severity.Info,
84
+ analyzer: ANALYZER_NAME,
85
+ title: `Raw SQL usage in \`${usage.method}()\` — verify safety`,
86
+ description: `\`${usage.method}()\` contains a raw SQL fragment. Ensure no user input reaches this query.`,
87
+ file: file.path,
88
+ line: usage.line,
89
+ recommendation: 'If any part of this query can be user-influenced, use parameter bindings.',
90
+ cwe: 'CWE-89',
91
+ snippet: usage.snippet,
92
+ });
93
+ }
94
+ }
95
+ }
96
+
97
+ return findings;
98
+ }
99
+ }
@@ -0,0 +1,112 @@
1
+ import { isDocsFile } from './exclusions.js';
2
+ import { Analyzer, Finding, HavocConfig, ParsedFile, Severity } from '../types/index.js';
3
+
4
+ const UNESCAPED_OUTPUT_PATTERN = /\{!!\s*(.+?)\s*!!\}/g;
5
+
6
+ const TRUSTED_CONTEXT_PATTERNS = [
7
+ /config\s*\(/,
8
+ /asset\s*\(/,
9
+ /url\s*\(/,
10
+ /route\s*\(/,
11
+ /__\s*\(/,
12
+ /trans\s*\(/,
13
+ ];
14
+
15
+ const USER_CONTROLLED_PATTERNS = [
16
+ /\$[a-zA-Z_][a-zA-Z0-9_]*->(?:body|content|message|description|text|html|comment|bio|about|summary)/i,
17
+ /\$(?:body|content|message|description|text|html|comment|bio|about|summary)\b/i,
18
+ /request\(\)/i,
19
+ /->input\s*\(/i,
20
+ ];
21
+
22
+ const ESCAPE_FUNCTION_PATTERNS = [
23
+ /\be\s*\(/,
24
+ /htmlspecialchars\s*\(/,
25
+ /Purifier::/,
26
+ /clean\s*\(/,
27
+ /strip_tags\s*\(/,
28
+ ];
29
+
30
+ const FINDING_HIGH = 'HAVOC-XSS-001';
31
+ const FINDING_MED = 'HAVOC-XSS-002';
32
+ const ANALYZER_NAME = 'XssSurfaceAnalyzer';
33
+
34
+ function isTrustedFilePath(path: string): boolean {
35
+ return (
36
+ path.includes('/mail/') ||
37
+ path.includes('/emails/') ||
38
+ path.includes('/notifications/') ||
39
+ path.includes('\\mail\\') ||
40
+ path.includes('\\emails\\') ||
41
+ path.includes('/svg/') ||
42
+ path.includes('/icons/') ||
43
+ path.endsWith('.svg.blade.php')
44
+ );
45
+ }
46
+
47
+ function classifyExpression(expression: string, filePath: string): 'trusted' | 'user-controlled' | 'unknown' {
48
+ if (ESCAPE_FUNCTION_PATTERNS.some((re) => re.test(expression))) return 'trusted';
49
+ if (TRUSTED_CONTEXT_PATTERNS.some((re) => re.test(expression))) return 'trusted';
50
+ if (isTrustedFilePath(filePath)) return 'trusted';
51
+ if (USER_CONTROLLED_PATTERNS.some((re) => re.test(expression))) return 'user-controlled';
52
+ return 'unknown';
53
+ }
54
+
55
+ export class XssSurfaceAnalyzer implements Analyzer {
56
+ readonly name = ANALYZER_NAME;
57
+ readonly description = 'Identifies unescaped Blade output that could lead to Cross-Site Scripting (XSS)';
58
+
59
+ async analyze(files: ParsedFile[], _config: HavocConfig): Promise<Finding[]> {
60
+ const findings: Finding[] = [];
61
+
62
+ for (const file of files) {
63
+ if (!file.path.endsWith('.blade.php') || isDocsFile(file.path)) continue;
64
+
65
+ const lines = file.content.split('\n');
66
+ lines.forEach((lineText, idx) => {
67
+ const lineNum = idx + 1;
68
+ const re = new RegExp(UNESCAPED_OUTPUT_PATTERN.source, 'g');
69
+ let match: RegExpExecArray | null;
70
+
71
+ while ((match = re.exec(lineText)) !== null) {
72
+ const expression = match[1];
73
+ const classification = classifyExpression(expression, file.path);
74
+ if (classification === 'trusted') continue;
75
+
76
+ if (classification === 'user-controlled') {
77
+ findings.push({
78
+ id: FINDING_HIGH,
79
+ severity: Severity.High,
80
+ analyzer: ANALYZER_NAME,
81
+ title: 'Unescaped user-controlled output in Blade template',
82
+ description:
83
+ `\`{!! ${expression} !!}\` outputs potentially user-controlled content without HTML escaping.`,
84
+ file: file.path,
85
+ line: lineNum,
86
+ recommendation: 'Use `{{ $variable }}` for automatic escaping.',
87
+ cwe: 'CWE-79',
88
+ snippet: lineText.trim(),
89
+ });
90
+ } else {
91
+ findings.push({
92
+ id: FINDING_MED,
93
+ severity: Severity.Medium,
94
+ analyzer: ANALYZER_NAME,
95
+ title: 'Unescaped output in Blade template — verify safety',
96
+ description:
97
+ `\`{!! ${expression} !!}\` outputs content without HTML escaping. Verify this cannot contain user HTML.`,
98
+ file: file.path,
99
+ line: lineNum,
100
+ recommendation:
101
+ 'If the value could contain user input, switch to `{{ $variable }}`.',
102
+ cwe: 'CWE-79',
103
+ snippet: lineText.trim(),
104
+ });
105
+ }
106
+ }
107
+ });
108
+ }
109
+
110
+ return findings;
111
+ }
112
+ }
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Shared file exclusion patterns for analyzers.
3
+ * Reduces false positives by skipping files that aren't relevant.
4
+ */
5
+
6
+ /** Files that should never be scanned for security vulnerabilities */
7
+ const EXCLUDED_PATTERNS = [
8
+ /database\/migrations\//,
9
+ /database\/seeders\//,
10
+ /database\/factories\//,
11
+ ];
12
+
13
+ /** Files that contain test/example code, not production code */
14
+ const TEST_PATTERNS = [
15
+ /tests?\//,
16
+ /spec\//,
17
+ /__tests__\//,
18
+ /\.test\./,
19
+ /\.spec\./,
20
+ ];
21
+
22
+ /** Documentation files */
23
+ const DOCS_PATTERNS = [
24
+ /docs?\//,
25
+ /documentation\//,
26
+ /examples?\//,
27
+ /demo\//,
28
+ ];
29
+
30
+ /** Model directories (for model-specific analyzers) */
31
+ const MODEL_PATTERNS = [
32
+ /app\/Models\//,
33
+ ];
34
+
35
+ export function isExcludedPath(path: string): boolean {
36
+ return EXCLUDED_PATTERNS.some(p => p.test(path));
37
+ }
38
+
39
+ export function isTestFile(path: string): boolean {
40
+ return TEST_PATTERNS.some(p => p.test(path));
41
+ }
42
+
43
+ export function isDocsFile(path: string): boolean {
44
+ return DOCS_PATTERNS.some(p => p.test(path));
45
+ }
46
+
47
+ export function isModelFile(path: string): boolean {
48
+ return MODEL_PATTERNS.some(p => p.test(path));
49
+ }
50
+
51
+ export function isBladeFile(path: string): boolean {
52
+ return path.endsWith('.blade.php');
53
+ }
54
+
55
+ /**
56
+ * Check if a raw SQL string is static (no PHP variable interpolation).
57
+ * Static SQL like selectRaw('MAX(id)') is safe.
58
+ */
59
+ export function isStaticSql(sqlString: string): boolean {
60
+ // Extract the string argument from the method call (the part inside quotes)
61
+ const stringArg = sqlString.match(/\(\s*['"](.*?)['"]\s*[,)]/s);
62
+ const sqlPart = stringArg ? stringArg[1] : sqlString;
63
+
64
+ // Contains PHP variable interpolation inside the SQL string
65
+ if (/\$[a-zA-Z_]/.test(sqlPart)) return false;
66
+ // Contains string concatenation with variable
67
+ if (/\.\s*\$/.test(sqlPart)) return false;
68
+ return true;
69
+ }
70
+
71
+ /**
72
+ * Check if a credential-like value is obviously fake/test.
73
+ */
74
+ export function isFakeCredential(value: string): boolean {
75
+ const fakePatterns = [
76
+ /super.?secret/i,
77
+ /test.?token/i,
78
+ /example[_\-]?(key|secret|token|password|value|here)/i, // e.g., example_key, example-token
79
+ /fake/i,
80
+ /dummy/i,
81
+ /placeholder/i,
82
+ /your.?key.?here/i,
83
+ /xxx+/i,
84
+ /000+/,
85
+ ];
86
+ return fakePatterns.some(p => p.test(value));
87
+ }
@@ -0,0 +1,15 @@
1
+ export { AuthorizationCoverageAnalyzer } from './AuthorizationCoverageAnalyzer.js';
2
+ export { MassAssignmentAnalyzer } from './MassAssignmentAnalyzer.js';
3
+ export { XssSurfaceAnalyzer } from './XssSurfaceAnalyzer.js';
4
+ export { SqlInjectionAnalyzer } from './SqlInjectionAnalyzer.js';
5
+ export { DependencyAuditAnalyzer } from './DependencyAuditAnalyzer.js';
6
+ export { IdorAnalyzer } from './IdorAnalyzer.js';
7
+ export { CredentialExposureAnalyzer } from './CredentialExposureAnalyzer.js';
8
+ export { SessionSecurityAnalyzer } from './SessionSecurityAnalyzer.js';
9
+ export { FileUploadAnalyzer } from './FileUploadAnalyzer.js';
10
+ export { RateLimitAnalyzer } from './RateLimitAnalyzer.js';
11
+ export { EncryptionAnalyzer } from './EncryptionAnalyzer.js';
12
+ export { PrivilegeEscalationAnalyzer } from './PrivilegeEscalationAnalyzer.js';
13
+ export { InsecureDeserializationAnalyzer } from './InsecureDeserializationAnalyzer.js';
14
+ export { OpenRedirectAnalyzer } from './OpenRedirectAnalyzer.js';
15
+ export { SecurityHeaderAnalyzer } from './SecurityHeaderAnalyzer.js';
package/src/index.ts ADDED
@@ -0,0 +1,226 @@
1
+ export * from './types/index.js';
2
+ export * from './analyzers/index.js';
3
+ export { PhpParser, PHP_FILE_PATTERNS } from './parsers/PhpParser.js';
4
+ export type { ParsedPhpFile, PhpClassInfo, PhpMethodInfo, PhpPropertyInfo } from './parsers/PhpParser.js';
5
+ export { RouteParser, buildRouteMap, getRouteMiddleware, isAuthorizationMiddleware, isAuthenticationMiddleware, hasAuthorizationMiddleware, hasAuthenticationMiddleware, isWebhookHandler } from './parsers/RouteParser.js';
6
+ export type { RouteInfo, ParsedRouteFile } from './parsers/RouteParser.js';
7
+ export { rules } from './rules/index.js';
8
+
9
+ import { readFile } from 'node:fs/promises';
10
+ import { existsSync } from 'node:fs';
11
+ import { resolve, join } from 'node:path';
12
+ import { glob } from 'glob';
13
+ import { Analyzer, HavocConfig, ParsedFile, ScanResult, Severity } from './types/index.js';
14
+ import { AuthorizationCoverageAnalyzer } from './analyzers/AuthorizationCoverageAnalyzer.js';
15
+ import { MassAssignmentAnalyzer } from './analyzers/MassAssignmentAnalyzer.js';
16
+ import { XssSurfaceAnalyzer } from './analyzers/XssSurfaceAnalyzer.js';
17
+ import { SqlInjectionAnalyzer } from './analyzers/SqlInjectionAnalyzer.js';
18
+ import { DependencyAuditAnalyzer } from './analyzers/DependencyAuditAnalyzer.js';
19
+ import { IdorAnalyzer } from './analyzers/IdorAnalyzer.js';
20
+ import { CredentialExposureAnalyzer } from './analyzers/CredentialExposureAnalyzer.js';
21
+ import { SessionSecurityAnalyzer } from './analyzers/SessionSecurityAnalyzer.js';
22
+ import { FileUploadAnalyzer } from './analyzers/FileUploadAnalyzer.js';
23
+ import { RateLimitAnalyzer } from './analyzers/RateLimitAnalyzer.js';
24
+ import { EncryptionAnalyzer } from './analyzers/EncryptionAnalyzer.js';
25
+ import { PrivilegeEscalationAnalyzer } from './analyzers/PrivilegeEscalationAnalyzer.js';
26
+ import { InsecureDeserializationAnalyzer } from './analyzers/InsecureDeserializationAnalyzer.js';
27
+ import { OpenRedirectAnalyzer } from './analyzers/OpenRedirectAnalyzer.js';
28
+ import { SecurityHeaderAnalyzer } from './analyzers/SecurityHeaderAnalyzer.js';
29
+ import { PhpParser } from './parsers/PhpParser.js';
30
+ import { RouteParser } from './parsers/RouteParser.js';
31
+ import type { ParsedRouteFile } from './parsers/RouteParser.js';
32
+
33
+ // ─── Security Grade ──────────────────────────────────────────────────────────
34
+
35
+ export type SecurityGrade = 'A' | 'B' | 'C' | 'D' | 'F';
36
+
37
+ interface GradeThreshold {
38
+ readonly grade: SecurityGrade;
39
+ readonly maxPenalty: number;
40
+ }
41
+
42
+ const SEVERITY_PENALTIES: Record<Severity, number> = {
43
+ [Severity.Critical]: 25,
44
+ [Severity.High]: 10,
45
+ [Severity.Medium]: 3,
46
+ [Severity.Low]: 1,
47
+ [Severity.Info]: 0,
48
+ };
49
+
50
+ const GRADE_THRESHOLDS: readonly GradeThreshold[] = [
51
+ { grade: 'A', maxPenalty: 0 },
52
+ { grade: 'B', maxPenalty: 10 },
53
+ { grade: 'C', maxPenalty: 25 },
54
+ { grade: 'D', maxPenalty: 50 },
55
+ { grade: 'F', maxPenalty: Infinity },
56
+ ];
57
+
58
+ export function calculateSecurityGrade(findings: { severity: Severity }[]): SecurityGrade {
59
+ const penalty = findings.reduce((sum, f) => sum + (SEVERITY_PENALTIES[f.severity] ?? 0), 0);
60
+ for (const threshold of GRADE_THRESHOLDS) {
61
+ if (penalty <= threshold.maxPenalty) return threshold.grade;
62
+ }
63
+ return 'F';
64
+ }
65
+
66
+ // ─── Defaults ─────────────────────────────────────────────────────────────────
67
+
68
+ export function createDefaultAnalyzers(routeFiles: ParsedRouteFile[] = []): Analyzer[] {
69
+ return [
70
+ new AuthorizationCoverageAnalyzer(routeFiles),
71
+ new MassAssignmentAnalyzer(),
72
+ new XssSurfaceAnalyzer(),
73
+ new SqlInjectionAnalyzer(),
74
+ new DependencyAuditAnalyzer(),
75
+ new IdorAnalyzer(),
76
+ new CredentialExposureAnalyzer(),
77
+ new SessionSecurityAnalyzer(),
78
+ new FileUploadAnalyzer(),
79
+ new RateLimitAnalyzer(),
80
+ new EncryptionAnalyzer(),
81
+ new PrivilegeEscalationAnalyzer(),
82
+ new InsecureDeserializationAnalyzer(),
83
+ new OpenRedirectAnalyzer(),
84
+ new SecurityHeaderAnalyzer(),
85
+ ];
86
+ }
87
+
88
+ /** @deprecated Use createDefaultAnalyzers() for route-aware analysis */
89
+ export const DEFAULT_ANALYZERS: Analyzer[] = createDefaultAnalyzers();
90
+
91
+ export const DEFAULT_CONFIG: HavocConfig = {
92
+ framework: 'laravel',
93
+ failOn: Severity.High,
94
+ output: 'text',
95
+ };
96
+
97
+ const SCANNER_VERSION = '0.2.0';
98
+
99
+ const DEFAULT_EXCLUDE_PATTERNS = [
100
+ 'vendor/**', 'node_modules/**', 'storage/**',
101
+ 'bootstrap/cache/**', 'public/vendor/**', '.git/**',
102
+ ];
103
+
104
+ async function discoverProjectFiles(
105
+ projectPath: string,
106
+ config: HavocConfig,
107
+ parser: PhpParser,
108
+ ): Promise<ParsedFile[]> {
109
+ const absPath = resolve(projectPath);
110
+ const excludePatterns = [
111
+ ...DEFAULT_EXCLUDE_PATTERNS,
112
+ ...(config.exclude ?? []),
113
+ ].map((e) => `${absPath}/${e}`);
114
+
115
+ const phpFiles = await glob(`${absPath}/**/*.php`, { ignore: excludePatterns, nodir: true });
116
+ const bladeFiles = await glob(`${absPath}/resources/views/**/*.blade.php`, { ignore: excludePatterns, nodir: true });
117
+
118
+ const parsedPhp = await Promise.all(phpFiles.map((f) => parser.parseFile(f, absPath)));
119
+
120
+ const parsedBlade = await Promise.all(
121
+ bladeFiles
122
+ .filter((f) => !phpFiles.includes(f))
123
+ .map(async (f) => ({
124
+ path: f.replace(`${absPath}/`, ''),
125
+ content: await readFile(f, 'utf-8').catch(() => ''),
126
+ ast: null,
127
+ })),
128
+ );
129
+
130
+ const composerJsonPath = join(absPath, 'composer.json');
131
+ const composerFiles: ParsedFile[] = existsSync(composerJsonPath)
132
+ ? [{ path: 'composer.json', content: await readFile(composerJsonPath, 'utf-8').catch(() => ''), ast: null }]
133
+ : [];
134
+
135
+ return [...parsedPhp, ...parsedBlade, ...composerFiles];
136
+ }
137
+
138
+ async function discoverRouteFiles(projectPath: string): Promise<ParsedRouteFile[]> {
139
+ const absPath = resolve(projectPath);
140
+ const routesDir = join(absPath, 'routes');
141
+ if (!existsSync(routesDir)) return [];
142
+
143
+ const routeParser = new RouteParser();
144
+ return routeParser.parseDirectory(absPath);
145
+ }
146
+
147
+ function computeCoverage(files: ParsedFile[]) {
148
+ let totalRoutes = 0;
149
+ let coveredRoutes = 0;
150
+ let totalModels = 0;
151
+ let protectedModels = 0;
152
+
153
+ for (const file of files) {
154
+ if (file.path.startsWith('routes/') || file.path.startsWith('routes\\')) {
155
+ const routeMatches = file.content.match(/Route::\w+\s*\(/g) ?? [];
156
+ totalRoutes += routeMatches.length;
157
+ const authMatches = file.content.match(/->middleware\s*\(\s*['"]auth/g) ?? [];
158
+ coveredRoutes += authMatches.length;
159
+ }
160
+ if ((file.path.includes('app/Models') || file.path.includes('app\\Models')) && file.path.endsWith('.php')) {
161
+ totalModels++;
162
+ if (/\$fillable\s*=|protected\s+\$guarded\s*=\s*\[(?:[^\]]+)\]/.test(file.content)) {
163
+ protectedModels++;
164
+ }
165
+ }
166
+ }
167
+
168
+ const coveragePercent = totalRoutes > 0 ? Math.round((coveredRoutes / totalRoutes) * 100) : 100;
169
+ return { totalRoutes, coveredRoutes, coveragePercent, totalModels, protectedModels };
170
+ }
171
+
172
+ export async function scan(
173
+ files: ParsedFile[],
174
+ config: HavocConfig = DEFAULT_CONFIG,
175
+ analyzers: Analyzer[] = DEFAULT_ANALYZERS,
176
+ ): Promise<ScanResult> {
177
+ const startTime = Date.now();
178
+
179
+ const allFindings = await Promise.all(
180
+ analyzers.map((analyzer) => analyzer.analyze(files, config)),
181
+ );
182
+
183
+ const findings = allFindings.flat();
184
+ const coverage = computeCoverage(files);
185
+
186
+ return {
187
+ findings,
188
+ coverage,
189
+ metadata: {
190
+ scannedAt: new Date().toISOString(),
191
+ framework: config.framework,
192
+ filesScanned: files.length,
193
+ durationMs: Date.now() - startTime,
194
+ scannerVersion: SCANNER_VERSION,
195
+ },
196
+ };
197
+ }
198
+
199
+ export async function scanProject(
200
+ projectPath: string,
201
+ config: HavocConfig = DEFAULT_CONFIG,
202
+ analyzers?: Analyzer[],
203
+ ): Promise<ScanResult & { grade: SecurityGrade }> {
204
+ const parser = new PhpParser();
205
+
206
+ // Parse route files first so we can pass them to analyzers
207
+ const routeFiles = await discoverRouteFiles(projectPath);
208
+
209
+ // Build route-aware analyzers unless caller supplied their own
210
+ const effectiveAnalyzers = analyzers ?? createDefaultAnalyzers(routeFiles);
211
+
212
+ // If caller supplied analyzers, try to inject route data into any AuthorizationCoverageAnalyzer
213
+ if (analyzers) {
214
+ for (const a of analyzers) {
215
+ if (a instanceof AuthorizationCoverageAnalyzer) {
216
+ a.setRouteFiles(routeFiles);
217
+ }
218
+ }
219
+ }
220
+
221
+ const files = await discoverProjectFiles(projectPath, config, parser);
222
+ const configWithRoot = { ...config, projectRoot: resolve(projectPath) } as HavocConfig & { projectRoot: string };
223
+ const result = await scan(files, configWithRoot, effectiveAnalyzers);
224
+ const grade = calculateSecurityGrade(result.findings);
225
+ return { ...result, grade };
226
+ }