@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.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +22 -0
- package/dist/analyzers/AuthorizationCoverageAnalyzer.d.ts +7 -0
- package/dist/analyzers/AuthorizationCoverageAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/AuthorizationCoverageAnalyzer.js +100 -0
- package/dist/analyzers/AuthorizationCoverageAnalyzer.js.map +1 -0
- package/dist/analyzers/CredentialExposureAnalyzer.d.ts +11 -0
- package/dist/analyzers/CredentialExposureAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/CredentialExposureAnalyzer.js +262 -0
- package/dist/analyzers/CredentialExposureAnalyzer.js.map +1 -0
- package/dist/analyzers/DependencyAuditAnalyzer.d.ts +28 -0
- package/dist/analyzers/DependencyAuditAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/DependencyAuditAnalyzer.js +107 -0
- package/dist/analyzers/DependencyAuditAnalyzer.js.map +1 -0
- package/dist/analyzers/EncryptionAnalyzer.d.ts +7 -0
- package/dist/analyzers/EncryptionAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/EncryptionAnalyzer.js +170 -0
- package/dist/analyzers/EncryptionAnalyzer.js.map +1 -0
- package/dist/analyzers/FileUploadAnalyzer.d.ts +8 -0
- package/dist/analyzers/FileUploadAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/FileUploadAnalyzer.js +193 -0
- package/dist/analyzers/FileUploadAnalyzer.js.map +1 -0
- package/dist/analyzers/IdorAnalyzer.d.ts +7 -0
- package/dist/analyzers/IdorAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/IdorAnalyzer.js +91 -0
- package/dist/analyzers/IdorAnalyzer.js.map +1 -0
- package/dist/analyzers/MassAssignmentAnalyzer.d.ts +7 -0
- package/dist/analyzers/MassAssignmentAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/MassAssignmentAnalyzer.js +90 -0
- package/dist/analyzers/MassAssignmentAnalyzer.js.map +1 -0
- package/dist/analyzers/PrivilegeEscalationAnalyzer.d.ts +7 -0
- package/dist/analyzers/PrivilegeEscalationAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/PrivilegeEscalationAnalyzer.js +217 -0
- package/dist/analyzers/PrivilegeEscalationAnalyzer.js.map +1 -0
- package/dist/analyzers/RateLimitAnalyzer.d.ts +7 -0
- package/dist/analyzers/RateLimitAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/RateLimitAnalyzer.js +151 -0
- package/dist/analyzers/RateLimitAnalyzer.js.map +1 -0
- package/dist/analyzers/SessionSecurityAnalyzer.d.ts +10 -0
- package/dist/analyzers/SessionSecurityAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/SessionSecurityAnalyzer.js +295 -0
- package/dist/analyzers/SessionSecurityAnalyzer.js.map +1 -0
- package/dist/analyzers/SqlInjectionAnalyzer.d.ts +7 -0
- package/dist/analyzers/SqlInjectionAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/SqlInjectionAnalyzer.js +77 -0
- package/dist/analyzers/SqlInjectionAnalyzer.js.map +1 -0
- package/dist/analyzers/XssSurfaceAnalyzer.d.ts +7 -0
- package/dist/analyzers/XssSurfaceAnalyzer.d.ts.map +1 -0
- package/dist/analyzers/XssSurfaceAnalyzer.js +100 -0
- package/dist/analyzers/XssSurfaceAnalyzer.js.map +1 -0
- package/dist/analyzers/index.d.ts +13 -0
- package/dist/analyzers/index.d.ts.map +1 -0
- package/dist/analyzers/index.js +13 -0
- package/dist/analyzers/index.js.map +1 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +139 -0
- package/dist/index.js.map +1 -0
- package/dist/parsers/PhpParser.d.ts +56 -0
- package/dist/parsers/PhpParser.d.ts.map +1 -0
- package/dist/parsers/PhpParser.js +193 -0
- package/dist/parsers/PhpParser.js.map +1 -0
- package/dist/parsers/RouteParser.d.ts +87 -0
- package/dist/parsers/RouteParser.d.ts.map +1 -0
- package/dist/parsers/RouteParser.js +327 -0
- package/dist/parsers/RouteParser.js.map +1 -0
- package/dist/rules/index.d.ts +14 -0
- package/dist/rules/index.d.ts.map +1 -0
- package/dist/rules/index.js +9 -0
- package/dist/rules/index.js.map +1 -0
- package/dist/types/index.d.ts +137 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +13 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +30 -0
- package/package.json.bak +27 -0
- package/src/analyzers/AuthorizationCoverageAnalyzer.ts +213 -0
- package/src/analyzers/CredentialExposureAnalyzer.ts +312 -0
- package/src/analyzers/DependencyAuditAnalyzer.ts +135 -0
- package/src/analyzers/EncryptionAnalyzer.ts +195 -0
- package/src/analyzers/FileUploadAnalyzer.ts +239 -0
- package/src/analyzers/IdorAnalyzer.ts +118 -0
- package/src/analyzers/InsecureDeserializationAnalyzer.ts +212 -0
- package/src/analyzers/MassAssignmentAnalyzer.ts +105 -0
- package/src/analyzers/OpenRedirectAnalyzer.ts +149 -0
- package/src/analyzers/PrivilegeEscalationAnalyzer.ts +258 -0
- package/src/analyzers/RateLimitAnalyzer.ts +195 -0
- package/src/analyzers/SecurityHeaderAnalyzer.ts +263 -0
- package/src/analyzers/SessionSecurityAnalyzer.ts +342 -0
- package/src/analyzers/SqlInjectionAnalyzer.ts +99 -0
- package/src/analyzers/XssSurfaceAnalyzer.ts +112 -0
- package/src/analyzers/exclusions.ts +87 -0
- package/src/analyzers/index.ts +15 -0
- package/src/index.ts +226 -0
- package/src/parsers/PhpParser.ts +259 -0
- package/src/parsers/RouteParser.ts +384 -0
- package/src/rules/index.ts +16 -0
- package/src/types/index.ts +164 -0
- package/tests/EncryptionAnalyzer.test.ts +137 -0
- package/tests/PrivilegeEscalationAnalyzer.test.ts +141 -0
- package/tests/RateLimitAnalyzer.test.ts +112 -0
- package/tests/analyzers.test.ts +678 -0
- package/tests/auth-coverage-route-aware.test.ts +294 -0
- package/tests/credential-exposure.test.ts +142 -0
- package/tests/file-upload.test.ts +141 -0
- package/tests/fixtures/app/Http/Controllers/AdminController.php +19 -0
- package/tests/fixtures/app/Http/Controllers/PostController.php +49 -0
- package/tests/fixtures/app/Http/Controllers/PublicController.php +17 -0
- package/tests/fixtures/app/Models/Comment.php +11 -0
- package/tests/fixtures/app/Models/OpenModel.php +11 -0
- package/tests/fixtures/app/Models/Post.php +14 -0
- package/tests/fixtures/app/Models/SafeModel.php +10 -0
- package/tests/fixtures/app/Models/User.php +15 -0
- package/tests/fixtures/blade/mail.blade.php +8 -0
- package/tests/fixtures/blade/safe.blade.php +12 -0
- package/tests/fixtures/blade/vulnerable.blade.php +12 -0
- package/tests/fixtures/controllers/AdminController.php +19 -0
- package/tests/fixtures/controllers/PostController.php +49 -0
- package/tests/fixtures/controllers/PublicController.php +17 -0
- package/tests/fixtures/deserialization/safe.php +32 -0
- package/tests/fixtures/deserialization/unsafe.php +60 -0
- package/tests/fixtures/models/Comment.php +11 -0
- package/tests/fixtures/models/OpenModel.php +11 -0
- package/tests/fixtures/models/Post.php +14 -0
- package/tests/fixtures/models/SafeModel.php +10 -0
- package/tests/fixtures/models/User.php +15 -0
- package/tests/fixtures/redirect/safe.php +38 -0
- package/tests/fixtures/redirect/unsafe.php +39 -0
- package/tests/fixtures/routes/api.php +9 -0
- package/tests/fixtures/routes/web.php +18 -0
- package/tests/fixtures/security-headers/app/Http/Middleware/SecurityHeaders.php +24 -0
- package/tests/fixtures/security-headers/app/Providers/AppServiceProvider.php +16 -0
- package/tests/fixtures/sql/safe_queries.php +7 -0
- package/tests/fixtures/sql/vulnerable_queries.php +7 -0
- package/tests/new-analyzers.test.ts +373 -0
- package/tests/route-parser.test.ts +257 -0
- package/tests/scanner.test.ts +82 -0
- package/tests/session-security.test.ts +161 -0
- package/tests/types.test.ts +29 -0
- package/tsconfig.json +9 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Route-aware AuthorizationCoverageAnalyzer tests
|
|
3
|
+
*
|
|
4
|
+
* These tests validate that route-level middleware correctly reduces false positives:
|
|
5
|
+
* - can:* middleware → fully authorized → not flagged at all
|
|
6
|
+
* - auth-only middleware → downgraded from HIGH to LOW
|
|
7
|
+
* - No middleware → still HIGH
|
|
8
|
+
* - Webhook handler → not flagged
|
|
9
|
+
*/
|
|
10
|
+
import { describe, it, expect } from 'vitest';
|
|
11
|
+
import { resolve, dirname } from 'node:path';
|
|
12
|
+
import { fileURLToPath } from 'node:url';
|
|
13
|
+
import { AuthorizationCoverageAnalyzer } from '../src/analyzers/AuthorizationCoverageAnalyzer.js';
|
|
14
|
+
import { RouteParser } from '../src/parsers/RouteParser.js';
|
|
15
|
+
import { PhpParser } from '../src/parsers/PhpParser.js';
|
|
16
|
+
import { DEFAULT_CONFIG } from '../src/index.js';
|
|
17
|
+
import { Severity } from '../src/types/index.js';
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const FIXTURES = resolve(__dirname, 'fixtures');
|
|
21
|
+
|
|
22
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
async function parsedController(className: string, methods: string) {
|
|
25
|
+
const parser = new PhpParser();
|
|
26
|
+
const source = `<?php
|
|
27
|
+
namespace App\\Http\\Controllers;
|
|
28
|
+
class ${className} extends Controller {
|
|
29
|
+
${methods}
|
|
30
|
+
}`;
|
|
31
|
+
const { writeFile, mkdir } = await import('node:fs/promises');
|
|
32
|
+
const tmpDir = `/tmp/havoc-test-${className}/app/Http/Controllers`;
|
|
33
|
+
await mkdir(tmpDir, { recursive: true });
|
|
34
|
+
const tmpPath = `${tmpDir}/${className}.php`;
|
|
35
|
+
await writeFile(tmpPath, source, 'utf-8');
|
|
36
|
+
return parser.parseFile(tmpPath, `/tmp/havoc-test-${className}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── can:* middleware → fully authorized ─────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
describe('AuthorizationCoverageAnalyzer — route-level can:* middleware', () => {
|
|
42
|
+
it('does NOT flag a controller action with can:* middleware', async () => {
|
|
43
|
+
const routeParser = new RouteParser();
|
|
44
|
+
const routes = routeParser.parseContent(
|
|
45
|
+
`Route::get('/admin', [AdminController::class, 'index'])->middleware('can:view-admin');`,
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const file = await parsedController('AdminController', `
|
|
49
|
+
public function index() {
|
|
50
|
+
return view('admin.index');
|
|
51
|
+
}
|
|
52
|
+
`);
|
|
53
|
+
|
|
54
|
+
const analyzer = new AuthorizationCoverageAnalyzer([routes]);
|
|
55
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
56
|
+
|
|
57
|
+
const authFindings = findings.filter((f) => f.analyzer === 'AuthorizationCoverageAnalyzer' && f.id === 'HAVOC-AUTH-001');
|
|
58
|
+
expect(authFindings).toHaveLength(0);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('does NOT flag any action when entire group has can:* middleware', async () => {
|
|
62
|
+
const routeParser = new RouteParser();
|
|
63
|
+
const routes = routeParser.parseContent(`
|
|
64
|
+
Route::middleware(['can:manage-posts'])->group(function () {
|
|
65
|
+
Route::resource('posts', PostController::class);
|
|
66
|
+
});
|
|
67
|
+
`);
|
|
68
|
+
|
|
69
|
+
const file = await parsedController('PostController', `
|
|
70
|
+
public function index() { return Post::all(); }
|
|
71
|
+
public function store() { return Post::create([]); }
|
|
72
|
+
public function destroy() { }
|
|
73
|
+
`);
|
|
74
|
+
|
|
75
|
+
const analyzer = new AuthorizationCoverageAnalyzer([routes]);
|
|
76
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
77
|
+
const authFindings = findings.filter((f) => f.id === 'HAVOC-AUTH-001');
|
|
78
|
+
expect(authFindings).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// ─── auth-only middleware → downgrade to LOW ──────────────────────────────────
|
|
83
|
+
|
|
84
|
+
describe('AuthorizationCoverageAnalyzer — auth-only middleware', () => {
|
|
85
|
+
it('produces LOW severity (not HIGH) for auth-only middleware', async () => {
|
|
86
|
+
const routeParser = new RouteParser();
|
|
87
|
+
const routes = routeParser.parseContent(
|
|
88
|
+
`Route::get('/profile', [ProfileController::class, 'show'])->middleware('auth');`,
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const file = await parsedController('ProfileController', `
|
|
92
|
+
public function show() {
|
|
93
|
+
return auth()->user();
|
|
94
|
+
}
|
|
95
|
+
`);
|
|
96
|
+
|
|
97
|
+
const analyzer = new AuthorizationCoverageAnalyzer([routes]);
|
|
98
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
99
|
+
const authFindings = findings.filter((f) => f.id === 'HAVOC-AUTH-001');
|
|
100
|
+
|
|
101
|
+
expect(authFindings).toHaveLength(1);
|
|
102
|
+
expect(authFindings[0].severity).toBe(Severity.Low);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('produces LOW severity for auth:sanctum middleware', async () => {
|
|
106
|
+
const routeParser = new RouteParser();
|
|
107
|
+
const routes = routeParser.parseContent(
|
|
108
|
+
`Route::get('/api/profile', [ApiProfileController::class, 'show'])->middleware('auth:sanctum');`,
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const file = await parsedController('ApiProfileController', `
|
|
112
|
+
public function show() { return response()->json([]); }
|
|
113
|
+
`);
|
|
114
|
+
|
|
115
|
+
const analyzer = new AuthorizationCoverageAnalyzer([routes]);
|
|
116
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
117
|
+
const authFindings = findings.filter((f) => f.id === 'HAVOC-AUTH-001');
|
|
118
|
+
|
|
119
|
+
expect(authFindings).toHaveLength(1);
|
|
120
|
+
expect(authFindings[0].severity).toBe(Severity.Low);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('produces LOW for verified middleware', async () => {
|
|
124
|
+
const routeParser = new RouteParser();
|
|
125
|
+
const routes = routeParser.parseContent(
|
|
126
|
+
`Route::get('/dashboard', [DashboardController::class, 'index'])->middleware(['auth', 'verified']);`,
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
const file = await parsedController('DashboardController', `
|
|
130
|
+
public function index() { return view('dashboard'); }
|
|
131
|
+
`);
|
|
132
|
+
|
|
133
|
+
const analyzer = new AuthorizationCoverageAnalyzer([routes]);
|
|
134
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
135
|
+
const authFindings = findings.filter((f) => f.id === 'HAVOC-AUTH-001');
|
|
136
|
+
|
|
137
|
+
expect(authFindings).toHaveLength(1);
|
|
138
|
+
expect(authFindings[0].severity).toBe(Severity.Low);
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
// ─── No middleware → HIGH ─────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
describe('AuthorizationCoverageAnalyzer — no route middleware', () => {
|
|
145
|
+
it('still produces HIGH when no route middleware is provided', async () => {
|
|
146
|
+
const analyzer = new AuthorizationCoverageAnalyzer([]); // no routes
|
|
147
|
+
|
|
148
|
+
const file = await parsedController('ExposedController', `
|
|
149
|
+
public function show() {
|
|
150
|
+
return Post::all();
|
|
151
|
+
}
|
|
152
|
+
`);
|
|
153
|
+
|
|
154
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
155
|
+
const authFindings = findings.filter((f) => f.id === 'HAVOC-AUTH-001');
|
|
156
|
+
expect(authFindings).toHaveLength(1);
|
|
157
|
+
expect(authFindings[0].severity).toBe(Severity.High);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('still produces HIGH for controller not present in route file', async () => {
|
|
161
|
+
const routeParser = new RouteParser();
|
|
162
|
+
const routes = routeParser.parseContent(
|
|
163
|
+
`Route::get('/other', [OtherController::class, 'index'])->middleware('can:do-stuff');`,
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
const file = await parsedController('UnknownController', `
|
|
167
|
+
public function show() { return Post::find(1); }
|
|
168
|
+
`);
|
|
169
|
+
|
|
170
|
+
const analyzer = new AuthorizationCoverageAnalyzer([routes]);
|
|
171
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
172
|
+
const authFindings = findings.filter((f) => f.id === 'HAVOC-AUTH-001');
|
|
173
|
+
expect(authFindings).toHaveLength(1);
|
|
174
|
+
expect(authFindings[0].severity).toBe(Severity.High);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ─── Inline authorization still works ────────────────────────────────────────
|
|
179
|
+
|
|
180
|
+
describe('AuthorizationCoverageAnalyzer — inline authorization', () => {
|
|
181
|
+
it('does not flag a method with $this->authorize even without route middleware', async () => {
|
|
182
|
+
const file = await parsedController('SecureController', `
|
|
183
|
+
public function show() {
|
|
184
|
+
$this->authorize('view', Post::class);
|
|
185
|
+
return Post::find(1);
|
|
186
|
+
}
|
|
187
|
+
`);
|
|
188
|
+
|
|
189
|
+
const analyzer = new AuthorizationCoverageAnalyzer([]);
|
|
190
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
191
|
+
const authFindings = findings.filter((f) => f.id === 'HAVOC-AUTH-001');
|
|
192
|
+
expect(authFindings).toHaveLength(0);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('does not flag a method with Gate::allows', async () => {
|
|
196
|
+
const file = await parsedController('GateController', `
|
|
197
|
+
public function index() {
|
|
198
|
+
Gate::allows('view-admin');
|
|
199
|
+
return view('admin');
|
|
200
|
+
}
|
|
201
|
+
`);
|
|
202
|
+
|
|
203
|
+
const analyzer = new AuthorizationCoverageAnalyzer([]);
|
|
204
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
205
|
+
const authFindings = findings.filter((f) => f.id === 'HAVOC-AUTH-001');
|
|
206
|
+
expect(authFindings).toHaveLength(0);
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// ─── Webhook handler detection ────────────────────────────────────────────────
|
|
211
|
+
|
|
212
|
+
describe('AuthorizationCoverageAnalyzer — webhook handlers', () => {
|
|
213
|
+
it('does not flag a webhook controller that verifies signature via X-Signature header', async () => {
|
|
214
|
+
const file = await parsedController('StripeWebhookController', `
|
|
215
|
+
public function handle(Request $request) {
|
|
216
|
+
$sig = $request->header('X-Stripe-Signature');
|
|
217
|
+
$event = Webhook::constructEvent($payload, $sig, config('services.stripe.secret'));
|
|
218
|
+
return response()->json(['status' => 'ok']);
|
|
219
|
+
}
|
|
220
|
+
`);
|
|
221
|
+
|
|
222
|
+
const analyzer = new AuthorizationCoverageAnalyzer([]);
|
|
223
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
224
|
+
const authFindings = findings.filter((f) => f.id === 'HAVOC-AUTH-001');
|
|
225
|
+
expect(authFindings).toHaveLength(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('does not flag a webhook controller that uses hash_hmac verification', async () => {
|
|
229
|
+
const file = await parsedController('GitHubWebhookController', `
|
|
230
|
+
public function receive(Request $request) {
|
|
231
|
+
$computed = hash_hmac('sha256', $request->getContent(), config('services.github.secret'));
|
|
232
|
+
if (!hash_equals($computed, $request->header('X-Hub-Signature-256'))) {
|
|
233
|
+
abort(403);
|
|
234
|
+
}
|
|
235
|
+
return response()->json(['ok' => true]);
|
|
236
|
+
}
|
|
237
|
+
`);
|
|
238
|
+
|
|
239
|
+
const analyzer = new AuthorizationCoverageAnalyzer([]);
|
|
240
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
241
|
+
const authFindings = findings.filter((f) => f.id === 'HAVOC-AUTH-001');
|
|
242
|
+
expect(authFindings).toHaveLength(0);
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
// ─── Integration: fixture files ───────────────────────────────────────────────
|
|
247
|
+
|
|
248
|
+
describe('AuthorizationCoverageAnalyzer — integration with fixture files', () => {
|
|
249
|
+
it('fixture route file reduces PostController false positives', async () => {
|
|
250
|
+
const routeParser = new RouteParser();
|
|
251
|
+
const phpParser = new PhpParser();
|
|
252
|
+
|
|
253
|
+
const routes = await routeParser.parseFile(resolve(FIXTURES, 'routes/web.php'), FIXTURES);
|
|
254
|
+
|
|
255
|
+
const postFile = await phpParser.parseFile(
|
|
256
|
+
resolve(FIXTURES, 'app/Http/Controllers/PostController.php'),
|
|
257
|
+
FIXTURES,
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
// Without routes: store() and update() are HIGH
|
|
261
|
+
const analyzerNoRoutes = new AuthorizationCoverageAnalyzer([]);
|
|
262
|
+
const findingsNoRoutes = await analyzerNoRoutes.analyze([postFile], DEFAULT_CONFIG);
|
|
263
|
+
const highNoRoutes = findingsNoRoutes.filter((f) => f.severity === Severity.High && f.id === 'HAVOC-AUTH-001');
|
|
264
|
+
expect(highNoRoutes.length).toBeGreaterThan(0);
|
|
265
|
+
|
|
266
|
+
// With routes: store() and update() are behind auth group -> downgraded to LOW
|
|
267
|
+
const analyzerWithRoutes = new AuthorizationCoverageAnalyzer([routes]);
|
|
268
|
+
const findingsWithRoutes = await analyzerWithRoutes.analyze([postFile], DEFAULT_CONFIG);
|
|
269
|
+
const highWithRoutes = findingsWithRoutes.filter((f) => f.severity === Severity.High && f.id === 'HAVOC-AUTH-001');
|
|
270
|
+
expect(highWithRoutes.length).toBeLessThan(highNoRoutes.length);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it('setRouteFiles updates route data after construction', async () => {
|
|
274
|
+
const routeParser = new RouteParser();
|
|
275
|
+
const routes = routeParser.parseContent(
|
|
276
|
+
`Route::get('/items', [ItemController::class, 'index'])->middleware('can:view-items');`,
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
const file = await parsedController('ItemController', `
|
|
280
|
+
public function index() { return Item::all(); }
|
|
281
|
+
`);
|
|
282
|
+
|
|
283
|
+
const analyzer = new AuthorizationCoverageAnalyzer([]);
|
|
284
|
+
|
|
285
|
+
// Before setting routes: HIGH
|
|
286
|
+
const before = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
287
|
+
expect(before.filter((f) => f.id === 'HAVOC-AUTH-001')[0]?.severity).toBe(Severity.High);
|
|
288
|
+
|
|
289
|
+
// After setting routes: should be cleared
|
|
290
|
+
analyzer.setRouteFiles([routes]);
|
|
291
|
+
const after = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
292
|
+
expect(after.filter((f) => f.id === 'HAVOC-AUTH-001')).toHaveLength(0);
|
|
293
|
+
});
|
|
294
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { CredentialExposureAnalyzer } from '../src/analyzers/CredentialExposureAnalyzer.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 CredentialExposureAnalyzer();
|
|
12
|
+
|
|
13
|
+
// ─── CredentialExposureAnalyzer ───────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
describe('CredentialExposureAnalyzer', () => {
|
|
16
|
+
it('has correct name', () => {
|
|
17
|
+
expect(analyzer.name).toBe('CredentialExposureAnalyzer');
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns empty array for empty file list', async () => {
|
|
21
|
+
const result = await analyzer.analyze([], DEFAULT_CONFIG);
|
|
22
|
+
expect(result).toHaveLength(0);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// .env file committed
|
|
26
|
+
it('flags .env file committed to repo as CRITICAL', async () => {
|
|
27
|
+
const file = makeFile('.env', 'APP_KEY=base64:abc123\nDB_PASSWORD=secret');
|
|
28
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
29
|
+
expect(findings.some(f => f.id === 'HAVOC-CRED-001')).toBe(true);
|
|
30
|
+
expect(findings.find(f => f.id === 'HAVOC-CRED-001')?.severity).toBe(Severity.Critical);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('flags .env.production as committed', async () => {
|
|
34
|
+
const file = makeFile('.env.production', 'APP_KEY=base64:abc123');
|
|
35
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
36
|
+
expect(findings.some(f => f.id === 'HAVOC-CRED-001')).toBe(true);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('does NOT flag .env.example as a secret exposure', async () => {
|
|
40
|
+
const file = makeFile('.env.example', 'APP_KEY=\nDB_PASSWORD=');
|
|
41
|
+
// .env.example is not treated as an env file by isEnvFile(), so CRED-001 is not triggered
|
|
42
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
43
|
+
// .env.example should not trigger CRED-002 (hardcoded secrets) since values are empty
|
|
44
|
+
expect(findings.filter(f => f.id === 'HAVOC-CRED-002')).toHaveLength(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Hardcoded Stripe key
|
|
48
|
+
it('detects hardcoded Stripe live secret key as CRITICAL', async () => {
|
|
49
|
+
const file = makeFile('app/Services/PaymentService.php', `<?php
|
|
50
|
+
$key = 'sk_live_abcdefghijklmnopqrstuvwx123456';
|
|
51
|
+
`);
|
|
52
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
53
|
+
expect(findings.some(f => f.id === 'HAVOC-CRED-002' && f.severity === Severity.Critical)).toBe(true);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// AWS key
|
|
57
|
+
it('detects hardcoded AWS Access Key ID', async () => {
|
|
58
|
+
const file = makeFile('app/Services/S3Service.php', `<?php
|
|
59
|
+
$key = 'AKIAIOSFODNN7REALKEY';
|
|
60
|
+
`);
|
|
61
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
62
|
+
expect(findings.some(f => f.id === 'HAVOC-CRED-002')).toBe(true);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Clean code with env()
|
|
66
|
+
it('does NOT flag secrets read from env()', async () => {
|
|
67
|
+
const file = makeFile('config/services.php', `<?php
|
|
68
|
+
return [
|
|
69
|
+
'stripe' => ['key' => env('STRIPE_SECRET')],
|
|
70
|
+
];
|
|
71
|
+
`);
|
|
72
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
73
|
+
expect(findings.filter(f => f.id === 'HAVOC-CRED-002')).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
// APP_DEBUG=true
|
|
77
|
+
it('flags APP_DEBUG=true in .env as HIGH', async () => {
|
|
78
|
+
const file = makeFile('.env', 'APP_DEBUG=true\nAPP_ENV=production');
|
|
79
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
80
|
+
expect(findings.some(f => f.id === 'HAVOC-CRED-003' && f.severity === Severity.High)).toBe(true);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('does NOT flag APP_DEBUG=false', async () => {
|
|
84
|
+
const file = makeFile('.env', 'APP_DEBUG=false\nAPP_ENV=production');
|
|
85
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
86
|
+
expect(findings.filter(f => f.id === 'HAVOC-CRED-003')).toHaveLength(0);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// APP_ENV=local in production config
|
|
90
|
+
it('flags APP_ENV=local in .env.production as MEDIUM', async () => {
|
|
91
|
+
const file = makeFile('.env.production', 'APP_ENV=local\nAPP_DEBUG=false');
|
|
92
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
93
|
+
expect(findings.some(f => f.id === 'HAVOC-CRED-004' && f.severity === Severity.Medium)).toBe(true);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// debug => true in config/app.php
|
|
97
|
+
it("flags 'debug' => true hardcoded in config/app.php as HIGH", async () => {
|
|
98
|
+
const file = makeFile('config/app.php', `<?php
|
|
99
|
+
return [
|
|
100
|
+
'debug' => true,
|
|
101
|
+
'env' => env('APP_ENV', 'production'),
|
|
102
|
+
];
|
|
103
|
+
`);
|
|
104
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
105
|
+
expect(findings.some(f => f.id === 'HAVOC-CRED-005' && f.severity === Severity.High)).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("does NOT flag 'debug' => env('APP_DEBUG', false)", async () => {
|
|
109
|
+
const file = makeFile('config/app.php', `<?php
|
|
110
|
+
return [
|
|
111
|
+
'debug' => env('APP_DEBUG', false),
|
|
112
|
+
];
|
|
113
|
+
`);
|
|
114
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
115
|
+
expect(findings.filter(f => f.id === 'HAVOC-CRED-005')).toHaveLength(0);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Private key inline
|
|
119
|
+
it('detects PEM private key in PHP source as CRITICAL', async () => {
|
|
120
|
+
const file = makeFile('app/Services/JwtService.php', `<?php
|
|
121
|
+
$key = '-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEA...\n-----END RSA PRIVATE KEY-----';
|
|
122
|
+
`);
|
|
123
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
124
|
+
expect(findings.some(f => f.id === 'HAVOC-CRED-007' && f.severity === Severity.Critical)).toBe(true);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
// DB credentials
|
|
128
|
+
it('detects PDO instantiation with hardcoded password as CRITICAL', async () => {
|
|
129
|
+
const file = makeFile('app/Legacy/Database.php', `<?php
|
|
130
|
+
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', 'supersecretpassword');
|
|
131
|
+
`);
|
|
132
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
133
|
+
expect(findings.some(f => f.id === 'HAVOC-CRED-006' && f.severity === Severity.Critical)).toBe(true);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// Non-PHP files not scanned for secrets
|
|
137
|
+
it('does not scan blade templates for PHP secrets', async () => {
|
|
138
|
+
const file = makeFile('resources/views/home.blade.php', `password = 'hardcoded_secret'`);
|
|
139
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
140
|
+
expect(findings.filter(f => f.id === 'HAVOC-CRED-002')).toHaveLength(0);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { FileUploadAnalyzer } from '../src/analyzers/FileUploadAnalyzer.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 FileUploadAnalyzer();
|
|
12
|
+
|
|
13
|
+
describe('FileUploadAnalyzer', () => {
|
|
14
|
+
it('has correct name', () => {
|
|
15
|
+
expect(analyzer.name).toBe('FileUploadAnalyzer');
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it('returns empty for empty file list', async () => {
|
|
19
|
+
expect(await analyzer.analyze([], DEFAULT_CONFIG)).toHaveLength(0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Missing type validation
|
|
23
|
+
it('flags upload without mimes validation as HIGH', async () => {
|
|
24
|
+
const file = makeFile('app/Http/Controllers/UploadController.php', `<?php
|
|
25
|
+
class UploadController extends Controller {
|
|
26
|
+
public function store(Request $request) {
|
|
27
|
+
$request->validate(['file' => 'required|file|max:2048']);
|
|
28
|
+
$path = $request->file('file')->store('uploads');
|
|
29
|
+
return response()->json(['path' => $path]);
|
|
30
|
+
}
|
|
31
|
+
}`);
|
|
32
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
33
|
+
expect(findings.some(f => f.id === 'HAVOC-UPLOAD-001' && f.severity === Severity.High)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Missing size validation
|
|
37
|
+
it('flags upload without max: size rule as MEDIUM', async () => {
|
|
38
|
+
const file = makeFile('app/Http/Controllers/UploadController.php', `<?php
|
|
39
|
+
class UploadController extends Controller {
|
|
40
|
+
public function store(Request $request) {
|
|
41
|
+
$request->validate(['file' => 'required|file|mimes:jpg,png']);
|
|
42
|
+
$path = $request->file('file')->store('uploads');
|
|
43
|
+
return response()->json(['path' => $path]);
|
|
44
|
+
}
|
|
45
|
+
}`);
|
|
46
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
47
|
+
expect(findings.some(f => f.id === 'HAVOC-UPLOAD-002' && f.severity === Severity.Medium)).toBe(true);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Clean upload controller — full validation
|
|
51
|
+
it('does NOT flag an upload with proper mimes and max validation', async () => {
|
|
52
|
+
const file = makeFile('app/Http/Controllers/UploadController.php', `<?php
|
|
53
|
+
class UploadController extends Controller {
|
|
54
|
+
public function store(Request $request) {
|
|
55
|
+
$request->validate(['file' => 'required|file|mimes:jpg,png,pdf|max:10240']);
|
|
56
|
+
$path = $request->file('file')->store('uploads');
|
|
57
|
+
return response()->json(['path' => $path]);
|
|
58
|
+
}
|
|
59
|
+
}`);
|
|
60
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
61
|
+
expect(findings.filter(f => f.id === 'HAVOC-UPLOAD-001')).toHaveLength(0);
|
|
62
|
+
expect(findings.filter(f => f.id === 'HAVOC-UPLOAD-002')).toHaveLength(0);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Public disk storage
|
|
66
|
+
it('flags storage to public disk as MEDIUM', async () => {
|
|
67
|
+
const file = makeFile('app/Http/Controllers/AvatarController.php', `<?php
|
|
68
|
+
class AvatarController extends Controller {
|
|
69
|
+
public function update(Request $request) {
|
|
70
|
+
$request->validate(['avatar' => 'required|file|mimes:jpg,png|max:2048']);
|
|
71
|
+
$path = $request->file('avatar')->store('avatars', 'public');
|
|
72
|
+
return response()->json(['path' => $path]);
|
|
73
|
+
}
|
|
74
|
+
}`);
|
|
75
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
76
|
+
expect(findings.some(f => f.id === 'HAVOC-UPLOAD-003' && f.severity === Severity.Medium)).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Executable extension in mimes
|
|
80
|
+
it('flags php in allowed mimes as HIGH', async () => {
|
|
81
|
+
const file = makeFile('app/Http/Controllers/FileController.php', `<?php
|
|
82
|
+
class FileController extends Controller {
|
|
83
|
+
public function store(Request $request) {
|
|
84
|
+
$request->validate(['file' => 'required|file|mimes:php,txt|max:1024']);
|
|
85
|
+
$path = $request->file('file')->store('uploads');
|
|
86
|
+
return response()->json(['path' => $path]);
|
|
87
|
+
}
|
|
88
|
+
}`);
|
|
89
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
90
|
+
expect(findings.some(f => f.id === 'HAVOC-UPLOAD-004' && f.severity === Severity.High)).toBe(true);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Path from user input
|
|
94
|
+
it('flags $request->input("path") used in Storage call as HIGH', async () => {
|
|
95
|
+
const file = makeFile('app/Http/Controllers/DocumentController.php', `<?php
|
|
96
|
+
class DocumentController extends Controller {
|
|
97
|
+
public function store(Request $request) {
|
|
98
|
+
$path = $request->input('path');
|
|
99
|
+
Storage::put($path, $request->file('doc')->get());
|
|
100
|
+
}
|
|
101
|
+
}`);
|
|
102
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
103
|
+
expect(findings.some(f => f.id === 'HAVOC-UPLOAD-005' && f.severity === Severity.High)).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// Path from input not near storage — no false positive
|
|
107
|
+
it('does NOT flag $request->input("path") used in non-storage context', async () => {
|
|
108
|
+
const file = makeFile('app/Http/Controllers/PageController.php', `<?php
|
|
109
|
+
class PageController extends Controller {
|
|
110
|
+
public function show(Request $request) {
|
|
111
|
+
$path = $request->input('path');
|
|
112
|
+
return view($path);
|
|
113
|
+
}
|
|
114
|
+
}`);
|
|
115
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
116
|
+
expect(findings.filter(f => f.id === 'HAVOC-UPLOAD-005')).toHaveLength(0);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// Non-PHP files skipped
|
|
120
|
+
it('does not analyze blade templates for upload issues', async () => {
|
|
121
|
+
const file = makeFile('resources/views/upload.blade.php', `<form method="POST" enctype="multipart/form-data"></form>`);
|
|
122
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
123
|
+
expect(findings).toHaveLength(0);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// $request->hasFile variant
|
|
127
|
+
it('detects upload patterns using $request->hasFile()', async () => {
|
|
128
|
+
const file = makeFile('app/Http/Controllers/ProfileController.php', `<?php
|
|
129
|
+
class ProfileController extends Controller {
|
|
130
|
+
public function update(Request $request) {
|
|
131
|
+
if ($request->hasFile('avatar')) {
|
|
132
|
+
$path = $request->file('avatar')->storeAs('avatars', 'file.jpg');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}`);
|
|
136
|
+
const findings = await analyzer.analyze([file], DEFAULT_CONFIG);
|
|
137
|
+
// Should flag missing type and size validation
|
|
138
|
+
expect(findings.some(f => f.id === 'HAVOC-UPLOAD-001')).toBe(true);
|
|
139
|
+
expect(findings.some(f => f.id === 'HAVOC-UPLOAD-002')).toBe(true);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace App\Http\Controllers;
|
|
4
|
+
|
|
5
|
+
class AdminController extends Controller
|
|
6
|
+
{
|
|
7
|
+
public function dashboard()
|
|
8
|
+
{
|
|
9
|
+
Gate::authorize('admin');
|
|
10
|
+
return view('admin.dashboard');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public function settings()
|
|
14
|
+
{
|
|
15
|
+
// Fully covered with Gate
|
|
16
|
+
Gate::allows('manage-settings');
|
|
17
|
+
return view('admin.settings');
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace App\Http\Controllers;
|
|
4
|
+
|
|
5
|
+
use App\Models\Post;
|
|
6
|
+
use Illuminate\Http\Request;
|
|
7
|
+
|
|
8
|
+
class PostController extends Controller
|
|
9
|
+
{
|
|
10
|
+
public function __construct()
|
|
11
|
+
{
|
|
12
|
+
// constructors are excluded
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public function index()
|
|
16
|
+
{
|
|
17
|
+
$this->authorize('viewAny', Post::class);
|
|
18
|
+
return Post::all();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public function show(Post $post)
|
|
22
|
+
{
|
|
23
|
+
Gate::allows('view', $post);
|
|
24
|
+
return $post;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public function store(Request $request)
|
|
28
|
+
{
|
|
29
|
+
// Missing authorization — should be flagged
|
|
30
|
+
return Post::create($request->all());
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public function update(Request $request, Post $post)
|
|
34
|
+
{
|
|
35
|
+
// Missing authorization — should be flagged
|
|
36
|
+
$post->update($request->all());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public function destroy(Post $post)
|
|
40
|
+
{
|
|
41
|
+
$this->authorize('delete', $post);
|
|
42
|
+
$post->delete();
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
protected function someProtectedMethod()
|
|
46
|
+
{
|
|
47
|
+
// protected — not flagged
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
|
|
3
|
+
namespace App\Http\Controllers;
|
|
4
|
+
|
|
5
|
+
class PublicController extends Controller
|
|
6
|
+
{
|
|
7
|
+
public function home()
|
|
8
|
+
{
|
|
9
|
+
// Public page — no auth needed, but analyzer will flag it
|
|
10
|
+
return view('home');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
public function about()
|
|
14
|
+
{
|
|
15
|
+
return view('about');
|
|
16
|
+
}
|
|
17
|
+
}
|