88 * This is a legitimate PHP file per ADR-031 §"Document/ZIP generation":
99 * streaming a ZIP containing ndjson/csv/manifest/signature-verification cannot
1010 * be expressed declaratively. All heavy lifting (audit-trail query, HMAC chain
11- * verification) is delegated to OR's AuditTrailService — this controller is
12- * intentionally thin.
11+ * verification) is delegated to OR's AuditTrailMapper and AuditHashService —
12+ * this controller is intentionally thin.
1313 *
1414 * Per ADR-022: uses OR's audit-trail-query abstraction; does NOT maintain a
15- * local event store or write any audit entries itself (OR does that on the
16- * AuditPackExportController's OR-query call automatically).
15+ * local event store or write any audit entries itself.
1716 *
18- * Per ADR-008: the `compliance.audit_pack.exported` audit-trail entry is emitted
19- * automatically by OR when we call $auditTrailService->query() (OR records every
20- * audit-trail access as an audit event for non-repudiation).
17+ * Per ADR-008: the audit-pack export is recorded automatically by OR's audit
18+ * trail when the query is made.
2119 *
2220 * @category Controller
2321 * @package OCA\Scholiq\Controller
3735
3836namespace OCA \Scholiq \Controller ;
3937
40- use OCA \OpenRegister \Service \AuditTrailService ;
38+ use OCA \OpenRegister \Db \AuditTrailMapper ;
39+ use OCA \OpenRegister \Service \AuditHashService ;
4140use OCA \Scholiq \AppInfo \Application ;
4241use OCP \AppFramework \Controller ;
4342use OCP \AppFramework \Http ;
4443use OCP \AppFramework \Http \DataDownloadResponse ;
4544use OCP \AppFramework \Http \JSONResponse ;
45+ use OCP \IConfig ;
4646use OCP \IRequest ;
47- use Psr \Log \LoggerInterface ;
4847
4948/**
5049 * Streams the ADR-008 §6 audit-pack ZIP for compliance officers and auditors.
5150 *
5251 * Single method: export(). Accepts POST with {regulationSlug, dateFrom, dateTo},
53- * queries OR's audit trail, and returns a ZIP containing:
52+ * queries OR's audit trail via AuditTrailMapper, verifies the HMAC chain via
53+ * AuditHashService, and returns a ZIP containing:
5454 * - audit-trail.ndjson (one JSON object per line, all matching events)
5555 * - audit-trail.csv (flat CSV of the same events)
5656 * - manifest.json (tenant_id, period, regulation_slug, event_count,
5757 * signature_status, export_timestamp, key_fingerprint)
58- * - signature-verification.txt (OR's HMAC chain verification report)
58+ * - signature-verification.txt (HMAC chain verification report)
5959 */
6060class AuditPackExportController extends Controller
6161{
62- /**
63- * Event types included in the audit pack per ADR-008 §6.
64- *
65- * @var string[]
66- */
67- private const AUDIT_EVENT_TYPES = [
68- 'attestation.signed ' ,
69- 'attestation.revoked ' ,
70- 'credential.issued ' ,
71- 'credential.revoked ' ,
72- 'credential.expired ' ,
73- 'enrolment.completed ' ,
74- 'compliance.regulation.published ' ,
75- 'compliance.audit_pack.exported ' ,
76- 'xapi.statement.received ' ,
77- ];
78-
7962 /**
8063 * Constructor.
8164 *
82- * @param IRequest $request HTTP request.
83- * @param AuditTrailService $auditTrailService OR audit-trail abstraction.
84- * @param LoggerInterface $logger PSR logger.
65+ * @param IRequest $request HTTP request.
66+ * @param AuditTrailMapper $auditTrailMapper OR audit-trail database mapper.
67+ * @param AuditHashService $auditHashService OR HMAC chain verification service.
68+ * @param IConfig $config Nextcloud system config for tenant ID lookup.
8569 */
8670 public function __construct (
8771 IRequest $ request ,
88- private readonly AuditTrailService $ auditTrailService ,
89- private readonly LoggerInterface $ logger ,
72+ private readonly AuditTrailMapper $ auditTrailMapper ,
73+ private readonly AuditHashService $ auditHashService ,
74+ private readonly IConfig $ config ,
9075 ) {
9176 parent ::__construct (appName: Application::APP_ID , request: $ request );
9277 }//end __construct()
@@ -115,33 +100,56 @@ public function export(
115100 );
116101 }
117102
118- // Query OR's audit trail — OR auto-records the access as compliance.audit_pack.exported .
119- $ events = $ this -> auditTrailService -> query (
120- [
121- ' event_type ' => self :: AUDIT_EVENT_TYPES ,
122- ' regulationSlug ' => $ regulationSlug ,
123- ' period ' => [ $ dateFrom , $ dateTo ],
124- ]
125- );
103+ // Query OR's audit trail via the real mapper — filters available columns .
104+ // `action` maps to event type; `created` holds the timestamp.
105+ $ entries = $ this -> auditTrailMapper -> findAll (
106+ filters: [
107+ ' created ' => $ dateFrom . ' , ' . $ dateTo ,
108+ ],
109+ sort: [ ' created ' => ' ASC ' ]
110+ );
126111
127- // Retrieve HMAC chain verification report from OR.
128- $ verification = $ this ->auditTrailService ->verifyChain (
129- [
130- 'regulationSlug ' => $ regulationSlug ,
131- 'period ' => [$ dateFrom , $ dateTo ],
132- ]
133- );
112+ // Serialise AuditTrail entities to plain arrays.
113+ $ events = [];
114+ foreach ($ entries as $ entry ) {
115+ $ row = $ entry ->jsonSerialize ();
116+ // Apply regulation filter on the serialised data (changed JSON field).
117+ if (is_string ($ row ['changed ' ] ?? null ) === true ) {
118+ $ changed = (array ) json_decode ($ row ['changed ' ], associative: true );
119+ } else {
120+ $ changed = [];
121+ }
122+
123+ if ($ regulationSlug !== '' && ($ changed ['regulationSlug ' ] ?? '' ) !== $ regulationSlug ) {
124+ continue ;
125+ }
126+
127+ $ events [] = $ row ;
128+ }
129+
130+ // Verify HMAC chain for the full log (inline — AuditHashService::verifyChain
131+ // is the only real OR method for chain verification, accepting int IDs as bounds).
132+ $ verification = $ this ->auditHashService ->verifyChain ();
133+ if (($ verification ['valid ' ] ?? false ) === true ) {
134+ $ signatureStatus = 'valid ' ;
135+ } else {
136+ $ signatureStatus = 'broken ' ;
137+ }
138+
139+ $ keyFingerprint = $ verification ['keyFingerprint ' ] ?? 'unavailable ' ;
140+ if (array_key_exists ('brokenAt ' , $ verification ) === true ) {
141+ $ keyFingerprint = 'unavailable ' ;
142+ }
134143
135- $ keyFingerprint = $ verification ['keyFingerprint ' ] ?? 'unavailable ' ;
136- $ signatureStatus = $ verification ['status ' ] ?? 'unknown ' ;
137144 $ eventCount = count ($ events );
138145 $ exportTimestamp = (new \DateTimeImmutable ('now ' , new \DateTimeZone ('UTC ' )))->format (\DateTimeInterface::ATOM );
146+ $ tenantId = $ this ->config ->getSystemValue ('instanceid ' , 'unknown ' );
139147
140148 // Build the four required files.
141149 $ ndjson = $ this ->buildNdjson (events: $ events );
142150 $ csv = $ this ->buildCsv (events: $ events );
143151 $ manifestJson = $ this ->buildManifestJson (
144- tenantId: $ this -> auditTrailService -> getCurrentTenantId () ,
152+ tenantId: $ tenantId ,
145153 regulationSlug: $ regulationSlug ,
146154 dateFrom: $ dateFrom ,
147155 dateTo: $ dateTo ,
@@ -203,27 +211,27 @@ private function buildNdjson(array $events): string
203211 private function buildCsv (array $ events ): string
204212 {
205213 if (empty ($ events ) === true ) {
206- return "event_id,event_type,regulation_slug,subject_id,actor_id,occurred_at,signature \n" ;
214+ return "event_id,action,object,register,schema,user,created \n" ;
207215 }
208216
209217 $ handle = fopen ('php://memory ' , 'r+ ' );
210218 if ($ handle === false ) {
211219 return '' ;
212220 }
213221
214- fputcsv ($ handle , ['event_id ' , 'event_type ' , 'regulation_slug ' , 'subject_id ' , 'actor_id ' , 'occurred_at ' , 'signature ' ]);
222+ fputcsv ($ handle , ['event_id ' , 'action ' , 'object ' , 'register ' , 'schema ' , 'user ' , 'created ' ]);
215223
216224 foreach ($ events as $ event ) {
217225 fputcsv (
218226 $ handle ,
219227 [
220- $ event ['id ' ] ?? '' ,
221- $ event ['event_type ' ] ?? '' ,
222- $ event ['regulationSlug ' ] ?? '' ,
223- $ event ['subject_id ' ] ?? '' ,
224- $ event ['actor_id ' ] ?? '' ,
225- $ event ['occurred_at ' ] ?? '' ,
226- $ event ['signature ' ] ?? '' ,
228+ $ event ['uuid ' ] ?? '' ,
229+ $ event ['action ' ] ?? '' ,
230+ $ event ['object ' ] ?? '' ,
231+ $ event ['register ' ] ?? '' ,
232+ $ event ['schema ' ] ?? '' ,
233+ $ event ['user ' ] ?? '' ,
234+ $ event ['created ' ] ?? '' ,
227235 ]
228236 );
229237 }
@@ -238,7 +246,7 @@ private function buildCsv(array $events): string
238246 /**
239247 * Build the manifest.json content per ADR-008 §6.
240248 *
241- * @param string $tenantId Tenant UUID.
249+ * @param string $tenantId Tenant UUID or instanceid .
242250 * @param string $regulationSlug Regulation slug.
243251 * @param string $dateFrom Period start.
244252 * @param string $dateTo Period end.
@@ -275,40 +283,42 @@ private function buildManifestJson(
275283 }//end buildManifestJson()
276284
277285 /**
278- * Build the signature-verification.txt content from OR's chain report .
286+ * Build the signature-verification.txt content from OR's chain verification result .
279287 *
280- * @param array<string,mixed> $verification OR verification response.
288+ * @param array<string,mixed> $verification OR AuditHashService::verifyChain() response.
281289 *
282290 * @return string Plain-text verification report.
283291 */
284292 private function buildVerificationTxt (array $ verification ): string
285293 {
286- $ status = $ verification ['status ' ] ?? 'unknown ' ;
287- $ fingerprint = $ verification ['keyFingerprint ' ] ?? 'unavailable ' ;
288- $ checkedAt = $ verification ['checkedAt ' ] ?? 'unknown ' ;
289- $ totalEvents = $ verification ['totalEvents ' ] ?? 0 ;
290- $ brokenAt = $ verification ['firstBrokenAt ' ] ?? null ;
294+ $ valid = ($ verification ['valid ' ] ?? false ) === true ;
295+ if ($ valid === true ) {
296+ $ status = 'valid ' ;
297+ } else {
298+ $ status = 'broken ' ;
299+ }
300+
301+ $ entriesVerified = $ verification ['entriesVerified ' ] ?? 0 ;
302+ $ brokenAt = $ verification ['brokenAt ' ] ?? null ;
291303
292304 $ lines = [];
293305 $ lines [] = '=== Scholiq Compliance Audit Pack — Signature Verification Report === ' ;
294306 $ lines [] = '' ;
295307 $ lines [] = 'Status : ' .$ status ;
296- $ lines [] = 'Key fingerprint : ' .$ fingerprint ;
297- $ lines [] = 'Checked at : ' .$ checkedAt ;
298- $ lines [] = 'Total events : ' .$ totalEvents ;
308+ $ lines [] = 'Entries verified: ' .$ entriesVerified ;
299309
300310 if ($ brokenAt !== null ) {
301311 $ lines [] = '' ;
302- $ lines [] = 'WARNING: Chain integrity broken at event : ' .$ brokenAt ;
312+ $ lines [] = 'WARNING: Chain integrity broken at entry id : ' .$ brokenAt ;
303313 $ lines [] = 'This indicates a record was modified or deleted after recording. ' ;
304314 } else {
305315 $ lines [] = '' ;
306316 $ lines [] = 'All HMAC signatures verified. Evidence log is intact. ' ;
307317 }
308318
309319 $ lines [] = '' ;
310- $ lines [] = 'This report is generated by OpenRegister \'s audit-trail verification endpoint . ' ;
311- $ lines [] = 'For offline verification use the key fingerprint above with the NDJSON file . ' ;
320+ $ lines [] = 'This report is generated by OpenRegister \'s AuditHashService::verifyChain() . ' ;
321+ $ lines [] = 'For offline verification cross-reference the NDJSON file with the chain hashes . ' ;
312322
313323 return implode ("\n" , $ lines )."\n" ;
314324 }//end buildVerificationTxt()
0 commit comments