authorize('view', $investigation); $investigation->load(['owner', 'targets.creator', 'targets.findings.source']); return view('investigations.report', compact('investigation')); } public function csv(Request $request, Investigation $investigation): Response { $this->authorize('view', $investigation); $investigation->load(['targets.findings.source']); $rows = ["target_type,target_value,source,status,summary,ran_at\n"]; foreach ($investigation->targets as $target) { foreach ($target->findings as $finding) { $rows[] = implode(',', array_map( fn ($v) => '"'.str_replace('"', '""', $this->csvSafe($v)).'"', [ $target->type, $target->value, $finding->source->name, $finding->status, $finding->summary, $finding->ran_at, ] ))."\n"; } } $filename = 'investigation-'.$investigation->id.'-findings.csv'; return response(implode('', $rows), 200, [ 'Content-Type' => 'text/csv', 'Content-Disposition' => "attachment; filename=\"{$filename}\"", ]); } /** * Prefix values that Excel/Sheets would interpret as a formula, to * prevent CSV injection via user-controlled target values or finding * summaries pulled from external tool output. */ private function csvSafe(mixed $value): string { $value = (string) $value; return preg_match('/^[=+\-@\t\r]/', $value) ? "'".$value : $value; } public function json(Request $request, Investigation $investigation): JsonResponse { $this->authorize('view', $investigation); $investigation->load(['owner', 'targets.creator', 'targets.findings.source']); $filename = 'investigation-'.$investigation->id.'.json'; return response()->json([ 'name' => $investigation->name, 'description' => $investigation->description, 'status' => $investigation->status, 'owner' => $investigation->owner->name, 'created_at' => $investigation->created_at, 'targets' => $investigation->targets->map(fn ($target) => [ 'type' => $target->type, 'value' => $target->value, 'notes' => $target->notes, 'created_by' => $target->creator->name, 'findings' => $target->findings->map(fn ($finding) => [ 'source' => $finding->source->name, 'status' => $finding->status, 'summary' => $finding->summary, 'raw_payload' => $finding->raw_payload, 'ran_at' => $finding->ran_at, ]), ]), ])->withHeaders([ 'Content-Disposition' => "attachment; filename=\"{$filename}\"", ]); } }