@@ -381,6 +381,107 @@ describe('AuditLogInterceptor', () => {
381381 } ) ;
382382 } ) ;
383383
384+ it ( 'should skip read endpoints that use a mutation verb (POST with read-only permission)' , ( done ) => {
385+ // e.g. POST /v1/trust-portal/documents/list — a list/status read that uses
386+ // POST to carry a filter body. It declares `read`, so it must not be logged
387+ // as "Created trust".
388+ jest . spyOn ( reflector , 'getAllAndOverride' ) . mockImplementation ( ( key ) => {
389+ if ( key === PERMISSIONS_KEY ) {
390+ return [ { resource : 'trust' , actions : [ 'read' ] } ] ;
391+ }
392+ if ( key === SKIP_AUDIT_LOG_KEY ) return false ;
393+ return undefined ;
394+ } ) ;
395+
396+ const context = createMockExecutionContext ( {
397+ method : 'POST' ,
398+ url : '/v1/trust-portal/documents/list' ,
399+ params : { } ,
400+ body : { organizationId : 'org_123' } ,
401+ } ) ;
402+ const handler = createMockCallHandler ( [ ] ) ;
403+
404+ interceptor . intercept ( context , handler ) . subscribe ( {
405+ next : ( ) => {
406+ setTimeout ( ( ) => {
407+ expect ( mockCreate ) . not . toHaveBeenCalled ( ) ;
408+ done ( ) ;
409+ } , 50 ) ;
410+ } ,
411+ } ) ;
412+ } ) ;
413+
414+ it ( 'still logs when only ONE of several declared permissions is read-only' , ( done ) => {
415+ // A POST that declares [read, create] is a real mutation — the read
416+ // requirement must not suppress the audit entry.
417+ jest . spyOn ( reflector , 'getAllAndOverride' ) . mockImplementation ( ( key ) => {
418+ if ( key === PERMISSIONS_KEY ) {
419+ return [
420+ { resource : 'trust' , actions : [ 'read' ] } ,
421+ { resource : 'policy' , actions : [ 'create' ] } ,
422+ ] ;
423+ }
424+ if ( key === SKIP_AUDIT_LOG_KEY ) return false ;
425+ return undefined ;
426+ } ) ;
427+
428+ const context = createMockExecutionContext ( {
429+ method : 'POST' ,
430+ url : '/v1/something' ,
431+ params : { } ,
432+ body : { organizationId : 'org_123' } ,
433+ } ) ;
434+ const handler = createMockCallHandler ( { id : 'ent_new' } ) ;
435+
436+ interceptor . intercept ( context , handler ) . subscribe ( {
437+ next : ( ) => {
438+ setTimeout ( ( ) => {
439+ expect ( mockCreate ) . toHaveBeenCalled ( ) ;
440+ done ( ) ;
441+ } , 50 ) ;
442+ } ,
443+ } ) ;
444+ } ) ;
445+
446+ it ( 'logs the action but never the payload for the secret resource' , ( done ) => {
447+ // Reading audit logs needs only app:read; the secret manager's plaintext
448+ // `value` must not be diffed into the log where an auditor (no secret:read)
449+ // could read it.
450+ jest . spyOn ( reflector , 'getAllAndOverride' ) . mockImplementation ( ( key ) => {
451+ if ( key === PERMISSIONS_KEY ) {
452+ return [ { resource : 'secret' , actions : [ 'create' ] } ] ;
453+ }
454+ if ( key === SKIP_AUDIT_LOG_KEY ) return false ;
455+ return undefined ;
456+ } ) ;
457+
458+ const context = createMockExecutionContext ( {
459+ method : 'POST' ,
460+ url : '/v1/secrets' ,
461+ params : { } ,
462+ body : { name : 'STRIPE_KEY' , value : 'sk_live_super_secret' } ,
463+ } ) ;
464+ const handler = createMockCallHandler ( { id : 'sec_1' } ) ;
465+
466+ interceptor . intercept ( context , handler ) . subscribe ( {
467+ next : ( ) => {
468+ setTimeout ( ( ) => {
469+ expect ( mockCreate ) . toHaveBeenCalled ( ) ;
470+ // The action is recorded...
471+ expect ( mockCreate ) . toHaveBeenCalledWith (
472+ expect . objectContaining ( {
473+ data : expect . objectContaining ( { description : 'Created secret' } ) ,
474+ } ) ,
475+ ) ;
476+ // ...but the plaintext value never appears anywhere in the row.
477+ const persisted = JSON . stringify ( mockCreate . mock . calls [ 0 ] [ 0 ] ) ;
478+ expect ( persisted ) . not . toContain ( 'sk_live_super_secret' ) ;
479+ done ( ) ;
480+ } , 50 ) ;
481+ } ,
482+ } ) ;
483+ } ) ;
484+
384485 it ( 'should skip requests without userId' , ( done ) => {
385486 jest . spyOn ( reflector , 'getAllAndOverride' ) . mockImplementation ( ( key ) => {
386487 if ( key === PERMISSIONS_KEY ) {
0 commit comments