Skip to content

Evidence Management

Status: Final Version: 1.0


Purpose

Define file handling, storage, retention, chain-of-custody, and immutability controls for evidence files supporting ESG metric submissions.


Evidence Types & File Formats

Category Evidence Type Code Allowed MIME Types Max Size Required For
Environmental UTILITY_BILL application/pdf, image/jpeg, image/png 10 MB Energy, water metrics
METER_READING image/*, application/pdf 5 MB Energy, water
LAB_REPORT application/pdf 10 MB Emissions, effluents
WASTE_MANIFEST application/pdf 10 MB Waste disposal
Social HR_REGISTER application/pdf, application/vnd.ms-excel, text/csv 25 MB Employment, diversity
TRAINING_RECORD application/pdf, image/* 10 MB Training hours
INCIDENT_REPORT application/pdf 10 MB Safety incidents
Governance BOARD_MINUTES application/pdf 10 MB Board diversity
POLICY_DOCUMENT application/pdf 10 MB Governance disclosures
AUDIT_REPORT application/pdf 25 MB Assurance

File Upload Workflow

1. Client Upload

POST /api/v1/collector/submissions/{uuid}/evidence
Content-Type: multipart/form-data

file: <binary>
evidence_type: UTILITY_BILL
description: March 2025 electricity bill

2. Backend Processing

class EvidenceService
{
    public function upload(MetricSubmission $submission, UploadedFile $file, string $evidenceType)
    {
        // 1. Validate file
        $this->validateFile($file, $evidenceType);

        // 2. Virus scan
        if (!$this->virusScan($file)) {
            throw new SecurityException('File failed virus scan');
        }

        // 3. Generate content hash
        $contentHash = hash_file('sha256', $file->path());

        // 4. Store immutably
        $path = $file->storeAs(
            "evidence/{$submission->tenant_id}/{$submission->id}",
            Str::uuid() . '.' . $file->extension(),
            'evidence' // Separate S3 bucket/filesystem
        );

        // 5. Create evidence record
        $evidence = Evidence::create([
            'tenant_id' => $submission->tenant_id,
            'filename' => $file->getClientOriginalName(),
            'filepath' => $path,
            'file_size' => $file->getSize(),
            'mime_type' => $file->getMimeType(),
            'content_hash' => $contentHash,
            'evidence_type' => $evidenceType,
            'description' => request('description'),
            'uploaded_by_user_id' => auth()->id(),
            'uploaded_from_ip' => request()->ip(),
            'uploaded_at' => now(),
        ]);

        // 6. Link to submission
        $submission->evidence()->attach($evidence->id);

        return $evidence;
    }

    protected function validateFile(UploadedFile $file, string $evidenceType)
    {
        $rules = config("evidence.types.{$evidenceType}");

        if (!in_array($file->getMimeType(), $rules['allowed_mime_types'])) {
            throw new ValidationException("File type not allowed for {$evidenceType}");
        }

        if ($file->getSize() > $rules['max_size_bytes']) {
            throw new ValidationException("File exceeds max size ({$rules['max_size_mb']} MB)");
        }
    }
}

Virus Scanning

Integration: ClamAV

use Xenolope\Quahog\Client as ClamAV;

protected function virusScan(UploadedFile $file): bool
{
    $clam = new ClamAV(config('clamav.socket'));
    $result = $clam->scanFile($file->path());

    if ($result['status'] === 'FOUND') {
        Log::alert('Virus detected', [
            'file' => $file->getClientOriginalName(),
            'virus' => $result['reason'],
            'user_id' => auth()->id(),
        ]);
        return false;
    }

    return true;
}

Storage Architecture

Laravel Filesystem Configuration

// config/filesystems.php
'disks' => [
    'evidence' => [
        'driver' => 's3',
        'key' => env('AWS_EVIDENCE_KEY'),
        'secret' => env('AWS_EVIDENCE_SECRET'),
        'region' => env('AWS_EVIDENCE_REGION'),
        'bucket' => env('AWS_EVIDENCE_BUCKET'),
        'url' => env('AWS_EVIDENCE_URL'),
        'visibility' => 'private', // Never public
        'options' => [
            'ServerSideEncryption' => 'AES256', // Encryption at rest
        ],
    ],
],

Signed URLs for Access

public function download(Evidence $evidence)
{
    $this->authorize('view', $evidence);

    // Log access for audit
    AuditLog::log('evidence.accessed', $evidence, auth()->user());

    // Generate temporary signed URL (1 hour expiry)
    return Storage::disk('evidence')->temporaryUrl($evidence->filepath, now()->addHour());
}

Retention Policy

Evidence Type Retention Period Legal Hold Support Auto-Delete After
All Evidence 7 years minimum ✅ Yes Never (manual only)
PII-Containing Subject to GDPR Right to Erasure ✅ Yes On request (anonymize)

Laravel Implementation

class Evidence extends Model
{
    protected $casts = [
        'legal_hold' => 'boolean',
        'retention_until' => 'date',
    ];

    public function canDelete(): bool
    {
        if ($this->legal_hold) {
            return false; // Cannot delete under legal hold
        }

        if ($this->retention_until && $this->retention_until->isFuture()) {
            return false; // Retention period not expired
        }

        return true;
    }
}

// Scheduled job: Mark evidence eligible for deletion
class MarkEvidenceForDeletionJob
{
    public function handle()
    {
        Evidence::where('retention_until', '<', now())
            ->where('legal_hold', false)
            ->whereNull('marked_for_deletion_at')
            ->update(['marked_for_deletion_at' => now()]);

        // Admin reviews and manually deletes
    }
}

Chain of Custody

Tracked Fields: - uploaded_by_user_id: Who uploaded - uploaded_from_ip: Source IP address - uploaded_at: Timestamp - content_hash: SHA-256 hash (immutability verification) - Access logs: Every download/view logged to audit_logs


Acceptance Criteria

  • File type whitelist enforced (MIME type check)
  • Max file size validated (per evidence type)
  • Virus scanning blocks infected files
  • Files stored with encryption at rest (S3 SSE)
  • Content hash generated and stored
  • Signed URLs expire after 1 hour
  • All evidence access logged to audit trail
  • Retention policy prevents premature deletion

Cross-References


Change Log

Version Date Author Changes
1.0 2026-01-03 Senior Product Architect Initial evidence management specification