Evidence Management
Status: Final Version: 1.2
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, water quality | |
WASTE_MANIFEST |
application/pdf |
10 MB | Waste disposal | |
FUEL_LOG |
application/pdf, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, text/csv |
15 MB | Diesel, petrol consumption | |
PRODUCTION_LOG |
application/pdf, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet, text/csv |
15 MB | Crushed ore, milled ore, gold production | |
WATER_METER_READING |
image/*, application/pdf |
5 MB | Water abstraction, consumption | |
EMISSIONS_MONITORING_REPORT |
application/pdf |
10 MB | Air quality monitoring (PM10, SO2, NO2, CO) | |
TSF_INSPECTION_REPORT |
application/pdf |
10 MB | TSF management (slurry density, freeboard, surface area, rate of rise) | |
REHAB_COMPLETION_RECORD |
application/pdf, image/* |
10 MB | Rehabilitation activities (MANDATORY for completed rehabilitation) | |
ENVIRONMENTAL_INCIDENT_REPORT |
application/pdf |
10 MB | Environmental incidents (MANDATORY for High/Critical severity incidents) | |
| 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 | General incidents | |
| Social (OHS) | ACCIDENT_REPORT |
application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document |
10 MB | Incident investigation, LTI, medical treatment, restricted work |
INCIDENT_REGISTER |
application/pdf, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
15 MB | Near miss, high potential incidents, property damage | |
CLINIC_REGISTER |
application/pdf, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
15 MB | Other clinic visits, first aid incidents | |
FIRST_AID_REGISTER |
application/pdf, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
15 MB | First aid incidents | |
MEDICAL_TREATMENT_RECORD |
application/pdf |
10 MB | Medical treatment incidents, LTI days | |
LTI_REGISTER |
application/pdf, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
15 MB | Lost time injuries, LTI days, LTIFR | |
FATALITY_REPORT |
application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document |
10 MB | Work-related fatalities (MANDATORY) | |
INVESTIGATION_REPORT |
application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document |
10 MB | Fatality investigations, high potential incidents | |
PROPERTY_DAMAGE_REPORT |
application/pdf, application/vnd.openxmlformats-officedocument.wordprocessingml.document |
10 MB | Property damage incidents | |
OCCUPATIONAL_DISEASE_REGISTER |
application/pdf, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
15 MB | Silicosis, pneumoconiosis cases | |
HSE_REPORT |
application/pdf |
10 MB | Health, safety, and environmental reports | |
TIME_SHEET |
application/pdf, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
15 MB | Hours worked for LTIFR calculation | |
PAYROLL_REPORT |
application/pdf, application/vnd.ms-excel, application/vnd.openxmlformats-officedocument.spreadsheetml.sheet |
25 MB | Hours worked verification | |
| Governance | BOARD_MINUTES |
application/pdf |
10 MB | Board diversity |
POLICY_DOCUMENT |
application/pdf |
10 MB | Governance disclosures | |
AUDIT_REPORT |
application/pdf |
25 MB | Assurance | |
| Community Investment | DONATION_RECEIPT |
application/pdf, image/* |
5 MB | Cash donations (MANDATORY if > threshold) |
CSR_INVOICE |
application/pdf |
10 MB | Direct spend on goods/services | |
FUEL_DONATION_LOG |
application/pdf, application/vnd.ms-excel, text/csv |
10 MB | In-kind fuel support | |
AGREEMENT_MOU |
application/pdf |
10 MB | Long-term partnerships, sponsorships | |
BENEFICIARY_LETTER |
application/pdf, image/* |
5 MB | Acknowledgement of receipt (in-kind) | |
ACTIVITY_PHOTO |
image/* |
10 MB | Evidence of event/handover (RECOMMENDED) |
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
@ApplicationScoped
class EvidenceService(
private val evidenceRepository: EvidenceRepository,
private val submissionRepository: MetricSubmissionRepository,
private val storageService: StorageService,
private val virusScanService: VirusScanService,
private val evidenceConfig: EvidenceConfig
) {
@Transactional
fun upload(
submission: MetricSubmission,
file: MultipartFile,
evidenceType: String,
description: String?,
currentUser: User,
clientIp: String
): Evidence {
// 1. Validate file
validateFile(file, evidenceType)
// 2. Virus scan
if (!virusScanService.scan(file)) {
throw SecurityException("File failed virus scan")
}
// 3. Generate content hash
val contentHash = MessageDigest.getInstance("SHA-256")
.digest(file.bytes)
.joinToString("") { "%02x".format(it) }
// 4. Store immutably
val extension = file.originalFilename?.substringAfterLast('.') ?: ""
val filename = "${UUID.randomUUID()}.$extension"
val path = "evidence/${submission.tenantId}/${submission.id}/$filename"
storageService.store(path, file.bytes)
// 5. Create evidence record
val evidence = Evidence(
id = UUID.randomUUID(),
tenantId = submission.tenantId,
filename = file.originalFilename ?: filename,
filepath = path,
fileSize = file.size,
mimeType = file.contentType ?: "application/octet-stream",
contentHash = contentHash,
evidenceType = evidenceType,
description = description,
uploadedByUserId = currentUser.id,
uploadedFromIp = clientIp,
uploadedAt = Instant.now()
)
evidenceRepository.persist(evidence)
// 6. Link to submission
submission.evidence.add(evidence)
submissionRepository.persist(submission)
return evidence
}
private fun validateFile(file: MultipartFile, evidenceType: String) {
val rules = evidenceConfig.types[evidenceType]
?: throw ValidationException("Unknown evidence type: $evidenceType")
if (file.contentType !in rules.allowedMimeTypes) {
throw ValidationException("File type not allowed for $evidenceType")
}
if (file.size > rules.maxSizeBytes) {
throw ValidationException("File exceeds max size (${rules.maxSizeMb} MB)")
}
}
}
Virus Scanning
Integration: ClamAV
@ApplicationScoped
class VirusScanService(
private val clamAvHost: String,
private val clamAvPort: Int,
private val logger: Logger = LoggerFactory.getLogger(VirusScanService::class.java)
) {
fun scan(file: MultipartFile): Boolean {
Socket(clamAvHost, clamAvPort).use { socket ->
val outputStream = socket.getOutputStream()
val inputStream = socket.getInputStream()
// Send INSTREAM command to ClamAV
outputStream.write("zINSTREAM\u0000".toByteArray())
// Stream file in chunks
file.bytes.chunked(2048).forEach { chunk ->
val size = ByteBuffer.allocate(4).putInt(chunk.size).array()
outputStream.write(size)
outputStream.write(chunk.toByteArray())
}
// Send zero-length chunk to signal end
outputStream.write(ByteArray(4))
outputStream.flush()
// Read result
val result = inputStream.bufferedReader().readLine()
if (result.contains("FOUND")) {
logger.warn(
"Virus detected in file: ${file.originalFilename}, result: $result",
mapOf(
"file" to file.originalFilename,
"scanResult" to result
)
)
return false
}
return true
}
}
}
Storage Architecture
Quarkus S3 Configuration
# application.properties
# Quarkus Amazon S3 Configuration (quarkus-amazon-s3 extension)
quarkus.s3.aws.region=${AWS_EVIDENCE_REGION:us-east-1}
quarkus.s3.aws.credentials.type=static
quarkus.s3.aws.credentials.static-provider.access-key-id=${AWS_EVIDENCE_KEY}
quarkus.s3.aws.credentials.static-provider.secret-access-key=${AWS_EVIDENCE_SECRET}
# S3 endpoint URL (optional, for LocalStack or custom endpoints)
quarkus.s3.endpoint-override=${AWS_EVIDENCE_URL:}
# Custom application properties
storage.evidence.bucket=${AWS_EVIDENCE_BUCKET}
storage.evidence.type=s3
storage.evidence.encryption=AES256
storage.evidence.visibility=private
# Profile-specific configuration
%dev.storage.evidence.type=local
%dev.quarkus.s3.endpoint-override=http://localhost:4566
%test.storage.evidence.type=local
Quarkus S3 Client Injection:
@ApplicationScoped
class S3StorageService(
private val s3Client: S3Client,
@ConfigProperty(name = "storage.evidence.bucket")
private val bucketName: String
) {
fun store(path: String, content: ByteArray) {
s3Client.putObject(
PutObjectRequest.builder()
.bucket(bucketName)
.key(path)
.serverSideEncryption(ServerSideEncryption.AES256)
.build(),
RequestBody.fromBytes(content)
)
}
fun generatePresignedUrl(path: String, duration: Duration): String {
val presigner = S3Presigner.builder()
.region(Region.of(System.getenv("AWS_EVIDENCE_REGION") ?: "us-east-1"))
.build()
val presignRequest = GetObjectPresignRequest.builder()
.signatureDuration(duration)
.getObjectRequest { it.bucket(bucketName).key(path) }
.build()
return presigner.presignGetObject(presignRequest).url().toString()
}
}
Signed URLs for Access
@Path("/api/v1/evidence")
@ApplicationScoped
class EvidenceResource(
private val storageService: StorageService,
private val evidenceRepository: EvidenceRepository,
private val evidenceAuthorizationService: EvidenceAuthorizationService,
private val auditLogRepository: AuditLogRepository,
private val securityIdentity: SecurityIdentity
) {
@GET
@Path("/{evidenceId}/download")
@RolesAllowed("EVIDENCE_VIEW")
@Produces(MediaType.TEXT_PLAIN)
fun download(
@PathParam("evidenceId") evidenceId: UUID
): Response {
val evidence = evidenceRepository.findByIdOptional(evidenceId)
.orElseThrow { NotFoundException("Evidence not found") }
// Get current user from SecurityIdentity
val userId = securityIdentity.principal.name
// Authorization check
if (!evidenceAuthorizationService.canView(userId, evidence)) {
throw ForbiddenException("Not authorized to view this evidence")
}
// Log access for audit
auditLogRepository.persist(
AuditLog(
action = "evidence.accessed",
entityType = "Evidence",
entityId = evidence.id,
userId = UUID.fromString(userId),
timestamp = Instant.now()
)
)
// Generate temporary signed URL (1 hour expiry)
val signedUrl = storageService.generatePresignedUrl(
evidence.filepath,
Duration.ofHours(1)
)
return Response.ok(signedUrl).build()
}
}
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) |
Retention Policy Implementation
@Entity
@Table(name = "evidence")
data class Evidence(
@Id
val id: UUID,
val tenantId: UUID,
val filename: String,
val filepath: String,
val fileSize: Long,
val mimeType: String,
val contentHash: String,
val evidenceType: String,
val description: String?,
val uploadedByUserId: UUID,
val uploadedFromIp: String,
val uploadedAt: Instant,
val legalHold: Boolean = false,
val retentionUntil: LocalDate? = null,
val markedForDeletionAt: Instant? = null
) {
fun canDelete(): Boolean {
if (legalHold) {
return false // Cannot delete under legal hold
}
if (retentionUntil != null && retentionUntil.isAfter(LocalDate.now())) {
return false // Retention period not expired
}
return true
}
}
// Scheduled task: Mark evidence eligible for deletion
@ApplicationScoped
class EvidenceRetentionScheduler(
private val evidenceRepository: EvidenceRepository
) {
@Scheduled(cron = "0 0 2 * * *") // Daily at 2 AM
fun markEvidenceForDeletion() {
val now = Instant.now()
val today = LocalDate.now()
val eligibleEvidence = evidenceRepository.findAll()
.filter { it.retentionUntil != null && it.retentionUntil!!.isBefore(today) }
.filter { !it.legalHold }
.filter { it.markedForDeletionAt == null }
eligibleEvidence.forEach { evidence ->
evidenceRepository.persist(evidence.copy(markedForDeletionAt = now))
}
// Admin reviews and manually deletes
}
}
OHS Evidence Requirements & Confidentiality
Overview
Occupational Health and Safety (OHS) evidence requires special handling due to the sensitive nature of incident data and potential inclusion of Personal Health Information (PHI). All OHS evidence must comply with confidentiality requirements and access controls defined in Security & Compliance.
Evidence Requirements by Incident Severity
OHS metrics follow a three-tier evidence requirement model based on incident severity and regulatory obligations:
| Incident Type | Severity Tier | Evidence Requirement | Minimum Evidence Types |
|---|---|---|---|
| Fatality | MANDATORY | Hard failure if missing (submission blocked) | FATALITY_REPORT + INVESTIGATION_REPORT (minimum 2 items) |
| Lost Time Injury (LTI) | MANDATORY | Hard failure if missing | LTI_REGISTER or ACCIDENT_REPORT (minimum 1 item) |
| High Potential Incident | MANDATORY | Hard failure if missing | INCIDENT_REGISTER or INVESTIGATION_REPORT (minimum 1 item) |
| Silicosis/Pneumoconiosis | MANDATORY | Hard failure if missing | OCCUPATIONAL_DISEASE_REGISTER or MEDICAL_TREATMENT_RECORD (minimum 1 item) |
| Medical Treatment | RECOMMENDED | Soft warning if missing (submission allowed) | MEDICAL_TREATMENT_RECORD or ACCIDENT_REPORT |
| Restricted Work | RECOMMENDED | Soft warning if missing | ACCIDENT_REPORT or HSE_REPORT |
| Property Damage | RECOMMENDED | Soft warning if missing | PROPERTY_DAMAGE_REPORT or INCIDENT_REGISTER |
| First Aid | OPTIONAL | No validation check | FIRST_AID_REGISTER or CLINIC_REGISTER |
| Near Miss | OPTIONAL | No validation check | INCIDENT_REGISTER or HSE_REPORT |
| Other Clinic Visits | OPTIONAL | No validation check | CLINIC_REGISTER |
Rationale: - MANDATORY tier: Incidents with severe consequences (fatalities, LTIs, high potential incidents, occupational diseases) require regulatory notification, investigation, and audit trail. Evidence is non-negotiable. - RECOMMENDED tier: Incidents requiring medical intervention or work restrictions should have supporting evidence for audit purposes, but lack of evidence does not block submission (allows data capture during emergencies). - OPTIONAL tier: Minor incidents and near misses are tracked for trending but do not require formal evidence (reduces collector burden).
Confidentiality Classification
All OHS evidence is classified as CONFIDENTIAL and must comply with the following requirements:
- Data Sensitivity:
- All incident data (aggregated counts, evidence files) is CONFIDENTIAL
-
Medical treatment records containing diagnosis or Personal Health Information (PHI) require additional protection beyond standard confidential classification
-
Encryption Requirements:
- At Rest: All OHS evidence files stored with S3 Server-Side Encryption (SSE-AES256)
- In Transit: TLS 1.2+ required for all uploads and downloads
-
Medical Records with PHI: Additional field-level encryption required (see Security & Compliance)
-
Anonymization Requirements:
- Aggregated Incident Counts: No individual employee names or identifiable information in metric submissions
- Evidence Files: Redact employee names, ID numbers, and medical diagnoses before upload
-
Fatality Reports: May contain victim information for regulatory notification, but must be restricted to authorized personnel only
-
Retention Requirements:
- Standard Retention: 7 years minimum (aligns with regulatory and audit requirements)
- Fatality Evidence: 10 years minimum (extended retention for legal and regulatory compliance)
- Medical Records with PHI: Subject to GDPR Right to Erasure (anonymize on request, keep aggregated counts)
Access Controls for OHS Evidence
OHS evidence access is restricted by role and follows the principle of least privilege:
| Role | Access Permissions | Use Case |
|---|---|---|
| Collector | Can upload OHS evidence for own site submissions only | Submit quarterly incident statistics and LTI performance data |
| Reviewer | Can view OHS evidence for assigned sites (site or business unit scope) | Validate evidence completeness and quality |
| Approver | Can view and approve OHS submissions and evidence for entire organisation | Formal sign-off on quarterly OHS reports |
| Admin | Full access to all OHS evidence (with mandatory access logging and audit justification) | System administration, troubleshooting, evidence retention management |
| Auditor | Read-only access to all OHS evidence for assurance purposes | External or internal audit, GRI 403 assurance |
| HSE Manager | Can view and export OHS evidence across organisation (read-only) | Analyse safety trends, prepare management reports |
Additional Controls:
- Evidence Access Logging: All views and downloads of OHS evidence are logged to audit_logs table with user ID, timestamp, IP address, and evidence ID
- Medical Records: Access restricted to HSE Manager, Approver, Admin, and Auditor roles only (Collectors and Reviewers cannot view medical records after upload)
- Fatality Evidence: Access restricted to Approver, Admin, Auditor, and HSE Manager only (requires additional audit justification)
- Regulatory Notification: Fatality reports may require external sharing with regulatory authorities (redact non-essential PHI before sharing)
Evidence Validation Rules
OHS evidence validation follows the tiered model with conditional checks based on incident counts:
// Example validation for OHS incident evidence
@ApplicationScoped
class OHSEvidenceValidator(
private val evidenceRepository: EvidenceRepository,
private val validationConfig: ValidationConfig
) {
fun validateIncidentEvidence(submission: MetricSubmission): List<ValidationError> {
val errors = mutableListOf<ValidationError>()
val rawData = submission.rawData as Map<String, Any>
val evidence = evidenceRepository.findBySubmissionId(submission.id)
val evidenceTypes = evidence.map { it.evidenceType }
// MANDATORY: Fatality evidence (minimum 2 items)
val fatalityCount = getIncidentCount(rawData, "fatality")
if (fatalityCount > 0) {
val hasFatalityReport = evidenceTypes.contains("FATALITY_REPORT")
val hasInvestigationReport = evidenceTypes.contains("INVESTIGATION_REPORT")
if (!hasFatalityReport || !hasInvestigationReport) {
errors.add(ValidationError(
"Fatality incidents require both FATALITY_REPORT and INVESTIGATION_REPORT (minimum 2 attachments)"
))
}
}
// MANDATORY: LTI evidence (minimum 1 item)
val ltiCount = getIncidentCount(rawData, "lti")
if (ltiCount > 0) {
val hasLtiEvidence = evidenceTypes.any { it in listOf("LTI_REGISTER", "ACCIDENT_REPORT") }
if (!hasLtiEvidence) {
errors.add(ValidationError(
"LTI incidents require at least one of: LTI_REGISTER, ACCIDENT_REPORT"
))
}
}
// RECOMMENDED: Medical treatment evidence (soft warning)
val medicalTreatmentCount = getIncidentCount(rawData, "medical_treatment")
if (medicalTreatmentCount > 0) {
val hasMedicalEvidence = evidenceTypes.any { it in listOf("MEDICAL_TREATMENT_RECORD", "ACCIDENT_REPORT") }
if (!hasMedicalEvidence) {
warnings.add(ValidationWarning(
"Medical treatment incidents should include MEDICAL_TREATMENT_RECORD or ACCIDENT_REPORT"
))
}
}
return errors
}
private fun getIncidentCount(rawData: Map<String, Any>, incidentType: String): Int {
// Extract mine + contractor count for incident type
val incidentData = rawData[incidentType] as? Map<String, Any> ?: return 0
val mine = incidentData["mine"] as? Int ?: 0
val contractors = incidentData["contractors"] as? Int ?: 0
return mine + contractors
}
}
PHI Redaction Guidance
Evidence files uploaded for OHS metrics must be redacted to protect Personal Health Information (PHI) and employee privacy:
Files Requiring Redaction: - Medical treatment records (diagnosis, prescription, treatment plan) - Fatality reports (victim name, next of kin contact information) - Occupational disease registers (employee name, medical history) - Accident reports (individual employee names, witness statements with names)
Acceptable Evidence Content: - Incident summary (date, time, location, type) - Root cause analysis and corrective actions - Aggregated statistics (total incidents, total LTI days) - Investigation findings and recommendations - Regulatory notification reference (case number, not individual details)
Redaction Process: 1. Use PDF redaction tools to permanently remove PHI (black boxes, not just highlights) 2. Export summary-only reports from HSE systems (exclude individual employee records) 3. Manually remove employee names and ID numbers from registers before upload 4. Add redaction note in evidence description: "Individual identifiers redacted for privacy"
Platform Feature (vNext): Automated PHI detection in uploaded files with warning prompts before upload.
Cross-References
- OHS Metric Catalog - OHS metric definitions and evidence requirements
- OHS Collection Templates - OHS data collection templates with evidence specifications
- OHS Validation Rules - OHS evidence validation implementation
- OHS Report Outputs - F.1 and F.2 OHS report sections
- Security & Compliance - OHS data confidentiality and access control requirements
Environmental Evidence Requirements & Confidentiality
Overview
Environmental evidence supports the collection and reporting of environmental metrics across production, materials consumption, energy usage, water consumption, air emissions, waste management, TSF management, rehabilitation activities, and environmental incidents. Environmental evidence requirements vary based on metric sensitivity and regulatory obligations.
Evidence Requirements by Metric Category
Environmental metrics follow a three-tier evidence requirement model based on regulatory requirements and data criticality:
| Metric Category | Evidence Tier | Evidence Requirement | Minimum Evidence Types |
|---|---|---|---|
| Environmental Incidents (High/Critical) | MANDATORY | Hard failure if missing (submission blocked) | ENVIRONMENTAL_INCIDENT_REPORT (minimum 1 item) |
| Rehabilitation Activities (Completed) | MANDATORY | Hard failure if missing | REHAB_COMPLETION_RECORD (minimum 1 item) |
| Water Quality Monitoring | RECOMMENDED | Soft warning if missing (submission allowed) | LAB_REPORT (water quality test results) |
| Air Emissions Monitoring | RECOMMENDED | Soft warning if missing | EMISSIONS_MONITORING_REPORT |
| Waste Disposal | RECOMMENDED | Soft warning if missing | WASTE_MANIFEST |
| TSF Management | RECOMMENDED | Soft warning if missing | TSF_INSPECTION_REPORT |
| Production Metrics | OPTIONAL | No validation check | PRODUCTION_LOG |
| Energy Metrics | OPTIONAL | No validation check | UTILITY_BILL, METER_READING, FUEL_LOG |
| Water Abstraction/Consumption | OPTIONAL | No validation check | WATER_METER_READING, UTILITY_BILL |
| Materials Consumption | OPTIONAL | No validation check | Invoices, stock cards, delivery notes |
Rationale: - MANDATORY tier: Environmental incidents with severe consequences (High/Critical severity) and completed rehabilitation activities require regulatory notification, documentation, and audit trail. Evidence is non-negotiable for compliance and assurance. - RECOMMENDED tier: Monitoring data (water quality, air emissions, waste, TSF) should have supporting evidence for audit purposes, but lack of evidence does not block submission (allows data capture when lab reports or inspection records are delayed). - OPTIONAL tier: Operational metrics (production, energy, water volumes, materials) are tracked for performance monitoring but do not require formal evidence (reduces collector burden for routine metrics).
Confidentiality Classification
Environmental evidence is classified based on sensitivity and regulatory/commercial considerations:
- Environmental Incidents (G.10):
- Classification: CONFIDENTIAL
- Rationale: High/Critical severity incidents may involve regulatory enforcement, reputational risk, and community impact. Incident details (descriptions, actions, root causes) are sensitive.
-
Access Restrictions: Evidence restricted to Approver, Admin, Auditor, and Environmental Manager roles only
-
Rehabilitation Activities (G.9):
- Classification: INTERNAL
- Rationale: Rehabilitation costs and completion status may be commercially sensitive but are generally disclosed in sustainability reports
-
Access Restrictions: Standard RBAC permissions (Collector, Reviewer, Approver, Admin, Auditor)
-
General Environmental Data (G.1-G.8):
- Classification: INTERNAL
- Rationale: Production, energy, water, emissions, waste, and TSF data are typically disclosed in sustainability reports (GRI 301-306) with organizational-level aggregation
- Access Restrictions: Standard RBAC permissions
Encryption Requirements
All environmental evidence files must comply with standard encryption requirements:
- At Rest: All evidence files stored with S3 Server-Side Encryption (SSE-AES256)
- In Transit: TLS 1.2+ required for all uploads and downloads
- High/Critical Incidents: Additional field-level encryption RECOMMENDED (SSE-KMS with customer-managed keys) for reputational risk management
Retention Requirements
| Evidence Category | Retention Period | Legal Hold Support | Auto-Delete After |
|---|---|---|---|
| Environmental Incidents (High/Critical) | 10 years minimum | ✅ Yes | Never (manual only) |
| Rehabilitation Completion Records | 10 years minimum | ✅ Yes | Never (manual only) |
| Water Quality Lab Reports | 7 years minimum | ✅ Yes | Never (manual only) |
| Emissions Monitoring Reports | 7 years minimum | ✅ Yes | Never (manual only) |
| TSF Inspection Reports | 7 years minimum | ✅ Yes | Never (manual only) |
| General Environmental Evidence | 7 years minimum | ✅ Yes | Never (manual only) |
Rationale: - Environmental incidents and rehabilitation records have extended retention (10 years) due to potential long-term regulatory and legal implications - Monitoring data (water quality, air emissions, TSF inspections) retained for 7 years to support regulatory compliance and audit requirements - All environmental evidence subject to legal hold (mining operations may face retrospective regulatory investigations)
Access Controls for Environmental Evidence
Environmental evidence access is restricted by role and follows the principle of least privilege:
| Role | Access Permissions | Use Case |
|---|---|---|
| Collector | Can upload environmental evidence for own site submissions only | Submit monthly production, materials, energy, water, waste, TSF data; quarterly water quality, air emissions, rehabilitation, and incident logs |
| Reviewer | Can view environmental evidence for assigned sites (site or business unit scope) | Validate evidence completeness and quality for assigned sites |
| Approver | Can view and approve environmental submissions and evidence for entire organisation | Formal sign-off on quarterly/annual environmental reports |
| Admin | Full access to all environmental evidence (with mandatory access logging and audit justification) | System administration, troubleshooting, evidence retention management |
| Auditor | Read-only access to all environmental evidence for assurance purposes | External or internal audit, GRI 301-307 assurance |
| Environmental Manager | Can view and export environmental evidence across organisation (read-only) | Analyse environmental trends, prepare management reports, regulatory compliance |
Additional Controls:
- Evidence Access Logging: All views and downloads of environmental evidence are logged to audit_logs table with user ID, timestamp, IP address, and evidence ID
- Incident Evidence (High/Critical): Access restricted to Approver, Admin, Auditor, and Environmental Manager roles only (Collectors and Reviewers cannot view high/critical incident evidence after upload)
- Regulatory Notification: Environmental incident reports may require external sharing with regulatory authorities (redact non-essential operational details before sharing)
Evidence Validation Rules
Environmental evidence validation follows the tiered model with conditional checks based on incident severity and rehabilitation status:
// Example validation for environmental incident evidence
@ApplicationScoped
class EnvironmentalEvidenceValidator(
private val evidenceRepository: EvidenceRepository,
private val validationConfig: ValidationConfig
) {
fun validateEnvironmentalEvidence(submission: MetricSubmission): List<ValidationError> {
val errors = mutableListOf<ValidationError>()
val rawData = submission.rawData as Map<String, Any>
val evidence = evidenceRepository.findBySubmissionId(submission.id)
val evidenceTypes = evidence.map { it.evidenceType }
// MANDATORY: High/Critical environmental incident evidence
val incidents = rawData["environmental_incidents"] as? List<Map<String, Any>> ?: emptyList()
val highCriticalIncidents = incidents.filter {
val severity = it["severity"] as? String ?: ""
severity in listOf("High", "Critical")
}
if (highCriticalIncidents.isNotEmpty()) {
val hasIncidentReport = evidenceTypes.contains("ENVIRONMENTAL_INCIDENT_REPORT")
if (!hasIncidentReport) {
errors.add(ValidationError(
"High/Critical environmental incidents require ENVIRONMENTAL_INCIDENT_REPORT"
))
}
}
// MANDATORY: Completed rehabilitation activity evidence
val rehabActivities = rawData["rehabilitation_activities"] as? List<Map<String, Any>> ?: emptyList()
val completedRehab = rehabActivities.filter {
val status = it["closure_status"] as? String ?: ""
status == "Closed"
}
if (completedRehab.isNotEmpty()) {
val hasCompletionRecord = evidenceTypes.contains("REHAB_COMPLETION_RECORD")
if (!hasCompletionRecord) {
errors.add(ValidationError(
"Completed rehabilitation activities require REHAB_COMPLETION_RECORD"
))
}
}
// RECOMMENDED: Water quality lab reports (soft warning)
val waterQuality = rawData["water_quality_monitoring"] as? List<Map<String, Any>> ?: emptyList()
if (waterQuality.isNotEmpty()) {
val hasLabReport = evidenceTypes.contains("LAB_REPORT")
if (!hasLabReport) {
warnings.add(ValidationWarning(
"Water quality monitoring should include LAB_REPORT with test results"
))
}
}
return errors
}
}
Cross-References
- Environmental Metric Catalog - Environmental metric definitions and evidence requirements
- Environmental Collection Templates - Environmental data collection templates with evidence specifications
- Environmental Validation Rules - Environmental evidence validation implementation
- Environmental Report Outputs - G.1-G.10 environmental report sections
- Security & Compliance - Environmental data confidentiality and access control requirements
Community Investment Evidence Requirements
Overview
Community Investment (CSR/CSIR) evidence validates financial and in-kind contributions to community development, education, health, and sports. Evidence requirements balance the need for financial auditability with the practical realities of community-based activities (where formal receipts may sometimes be replaced by acknowledgement letters).
Evidence Requirements by Contribution Type
Community Investment evidence requirements are determined by the nature of the contribution (Cash vs. In-Kind) and the value involved:
| Contribution Type | Evidence Requirement | Minimum Evidence Types |
|---|---|---|
| Cash Donation > Threshold | MANDATORY | Hard failure if missing |
| Cash Donation < Threshold | RECOMMENDED | Soft warning if missing |
| In-Kind (Goods/Services) | MANDATORY | Hard failure if missing |
| In-Kind (Fuel) | MANDATORY | Hard failure if missing |
| Sponsorship | MANDATORY | Hard failure if missing |
| Community Event | RECOMMENDED | Soft warning if missing |
Rationale: - Financial Auditability: Direct cash spend and purchasing of goods for donation require strict financial evidence (invoices, receipts) to prevent fraud and ensuring tax compliance. - In-Kind Verification: Fuel and goods handed over to beneficiaries require acknowledgement (Beneficiary Letter or Log) to prove the aid reached the intended recipient. - Visual Evidence: Photos are recommended to build a "portfolio of evidence" for sustainability reports but are not sufficient on their own for financial audit.
Confidentiality Classification
Community Investment evidence is generally classified as INTERNAL, with specific privacy considerations:
- Beneficiary Privacy:
- Classification: INTERNAL (Restricted)
- Requirement: Avoid capturing PII of individual beneficiaries (e.g., students, patients) in photos or lists unless consent is obtained.
-
Prefer: Aggregate beneficiary letters (e.g., from a School Principal) over lists of individual student names.
-
Financial Records:
- Classification: INTERNAL
-
Requirement: Invoices and receipts typically contain supplier and pricing details which are internal business records.
-
Strategic Partnerships:
- Classification: INTERNAL or CONFIDENTIAL (depending on MOU terms)
- Requirement: MOUs may contain confidentiality clauses regarding sponsorship values before public announcement.
Access Controls
| Role | Access Permissions | Use Case |
|---|---|---|
| Collector | Uploads evidence for site/org activities | Submitting quarterly CSR log entries |
| Reviewer | View access for validation | Verifying that spend matches evidence |
| Approver | View access for sign-off | Ensuring compliance with CSR policy |
| Auditor | Read-only access | verifying financial spend vs reported impact |
| External Stakeholders | No direct access | (Data is aggregated for public reports) |
Evidence Validation Rules
// Example validation for Community Investment evidence
@ApplicationScoped
class CommunityInvestmentEvidenceValidator(
private val evidenceRepository: EvidenceRepository
) {
fun validate(submission: CommunityInvestmentLog): List<ValidationError> {
val errors = mutableListOf<ValidationError>()
val evidence = evidenceRepository.findByEntityId(submission.id)
val evidenceTypes = evidence.map { it.evidenceType }
// Rule: Cash spend > $1000 requires Receipt or Invoice
if (submission.actualAmount > 1000.0 && submission.currency == "USD") {
val hasFinancialProof = evidenceTypes.any {
it in listOf("DONATION_RECEIPT", "CSR_INVOICE", "AGREEMENT_MOU")
}
if (!hasFinancialProof) {
errors.add(ValidationError("Cash spend over $1000 requires DONATION_RECEIPT, CSR_INVOICE, or AGREEMENT_MOU"))
}
}
// Rule: Fuel donations require Fuel Log
if (submission.description.contains("Fuel", ignoreCase = true)) {
if (!evidenceTypes.contains("FUEL_DONATION_LOG")) {
errors.add(ValidationError("Fuel donations require a FUEL_DONATION_LOG attachment"))
}
}
return errors
}
}
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.3 | 2026-01-21 | Ralph Agent | Added Community Investment Evidence section (types: DONATION_RECEIPT, CSR_INVOICE, FUEL_DONATION_LOG, AGREEMENT_MOU, BENEFICIARY_LETTER, ACTIVITY_PHOTO), defined mandatory/recommended tiers based on spend thresholds and in-kind nature, and documented confidentiality/privacy rules for beneficiary data. |
| 1.2 | 2026-01-19 | Ralph Agent | Added Environmental Evidence Requirements & Confidentiality section covering: 8 environmental evidence types (FUEL_LOG, PRODUCTION_LOG, WATER_METER_READING, EMISSIONS_MONITORING_REPORT, TSF_INSPECTION_REPORT, REHAB_COMPLETION_RECORD, ENVIRONMENTAL_INCIDENT_REPORT), evidence requirements by metric category (three-tier model: MANDATORY for High/Critical incidents and completed rehabilitation, RECOMMENDED for monitoring data, OPTIONAL for operational metrics), confidentiality classification (CONFIDENTIAL for incidents, INTERNAL for general environmental data), access controls by role (Collector, Reviewer, Approver, Admin, Auditor, Environmental Manager), retention requirements (10 years for incidents/rehabilitation, 7 years for monitoring data), evidence validation rules implementation, and cross-references to environmental sections. Extended Evidence Types table with 8 environmental evidence types. Updated LAB_REPORT description to include water quality. |
| 1.1 | 2026-01-18 | Ralph Agent | Added OHS Evidence Requirements & Confidentiality section covering: 14 OHS evidence types (ACCIDENT_REPORT, INCIDENT_REGISTER, CLINIC_REGISTER, FIRST_AID_REGISTER, MEDICAL_TREATMENT_RECORD, LTI_REGISTER, FATALITY_REPORT, INVESTIGATION_REPORT, PROPERTY_DAMAGE_REPORT, OCCUPATIONAL_DISEASE_REGISTER, HSE_REPORT, TIME_SHEET, PAYROLL_REPORT), evidence requirements by incident severity (three-tier model: MANDATORY, RECOMMENDED, OPTIONAL), confidentiality classification (all OHS data is CONFIDENTIAL, medical records with PHI require additional encryption), access controls by role (Collector, Reviewer, Approver, Admin, Auditor, HSE Manager), evidence validation rules implementation, PHI redaction guidance, and cross-references to OHS sections. Extended Evidence Types table with 14 OHS evidence types including allowed MIME types and max file sizes. |
| 1.0 | 2026-01-03 | Senior Product Architect | Initial evidence management specification |