## Summary - add a first-class finding exception domain with request, approval, rejection, renewal, and revocation lifecycle support - add tenant-scoped exception register, finding governance surfaces, and a canonical workspace approval queue in Filament - add audit, badge, evidence, and review-pack integrations plus focused Pest coverage for workflow, authorization, and governance validity ## Validation - vendor/bin/sail bin pint --dirty --format agent - CI=1 vendor/bin/sail artisan test --compact - manual integrated-browser smoke test for the request-exception happy path, tenant register visibility, and canonical queue visibility ## Notes - Filament implementation remains on v5 with Livewire v4-compatible surfaces - canonical queue lives in the admin panel; provider registration stays in bootstrap/providers.php - finding exceptions stay out of global search in this rollout Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #184
446 lines
13 KiB
YAML
446 lines
13 KiB
YAML
openapi: 3.1.0
|
|
info:
|
|
title: Finding Risk Acceptance Internal Contract
|
|
version: 0.1.0
|
|
description: |
|
|
Internal admin-plane contract for the Finding Risk Acceptance Lifecycle.
|
|
These endpoints represent the server-side action contract backing Filament and Livewire surfaces.
|
|
servers:
|
|
- url: /api/internal
|
|
paths:
|
|
/tenants/{tenantId}/findings/{findingId}/exception-requests:
|
|
post:
|
|
summary: Request exception
|
|
operationId: requestFindingException
|
|
tags: [Finding Exceptions]
|
|
parameters:
|
|
- $ref: '#/components/parameters/TenantId'
|
|
- $ref: '#/components/parameters/FindingId'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RequestExceptionInput'
|
|
responses:
|
|
'201':
|
|
description: Exception request created
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FindingExceptionResource'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
/tenants/{tenantId}/finding-exceptions:
|
|
get:
|
|
summary: List tenant exceptions
|
|
operationId: listTenantFindingExceptions
|
|
tags: [Finding Exceptions]
|
|
parameters:
|
|
- $ref: '#/components/parameters/TenantId'
|
|
- $ref: '#/components/parameters/StateFilter'
|
|
- $ref: '#/components/parameters/DueFilter'
|
|
responses:
|
|
'200':
|
|
description: Tenant exception register
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/FindingExceptionResource'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
/tenants/{tenantId}/finding-exceptions/{exceptionId}:
|
|
get:
|
|
summary: View exception detail
|
|
operationId: showFindingException
|
|
tags: [Finding Exceptions]
|
|
parameters:
|
|
- $ref: '#/components/parameters/TenantId'
|
|
- $ref: '#/components/parameters/ExceptionId'
|
|
responses:
|
|
'200':
|
|
description: Exception detail
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FindingExceptionDetail'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
/tenants/{tenantId}/finding-exceptions/{exceptionId}/approve:
|
|
post:
|
|
summary: Approve exception
|
|
operationId: approveFindingException
|
|
tags: [Finding Exceptions]
|
|
parameters:
|
|
- $ref: '#/components/parameters/TenantId'
|
|
- $ref: '#/components/parameters/ExceptionId'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ApproveExceptionInput'
|
|
responses:
|
|
'200':
|
|
description: Exception approved
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FindingExceptionResource'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
/tenants/{tenantId}/finding-exceptions/{exceptionId}/reject:
|
|
post:
|
|
summary: Reject exception
|
|
operationId: rejectFindingException
|
|
tags: [Finding Exceptions]
|
|
parameters:
|
|
- $ref: '#/components/parameters/TenantId'
|
|
- $ref: '#/components/parameters/ExceptionId'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RejectExceptionInput'
|
|
responses:
|
|
'200':
|
|
description: Exception rejected
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FindingExceptionResource'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
/tenants/{tenantId}/finding-exceptions/{exceptionId}/renew:
|
|
post:
|
|
summary: Renew exception
|
|
operationId: renewFindingException
|
|
tags: [Finding Exceptions]
|
|
parameters:
|
|
- $ref: '#/components/parameters/TenantId'
|
|
- $ref: '#/components/parameters/ExceptionId'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RenewExceptionInput'
|
|
responses:
|
|
'200':
|
|
description: Renewal request accepted
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FindingExceptionResource'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
/tenants/{tenantId}/finding-exceptions/{exceptionId}/revoke:
|
|
post:
|
|
summary: Revoke exception
|
|
operationId: revokeFindingException
|
|
tags: [Finding Exceptions]
|
|
parameters:
|
|
- $ref: '#/components/parameters/TenantId'
|
|
- $ref: '#/components/parameters/ExceptionId'
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RevokeExceptionInput'
|
|
responses:
|
|
'200':
|
|
description: Exception revoked
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/FindingExceptionResource'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
'422':
|
|
$ref: '#/components/responses/ValidationError'
|
|
/workspaces/{workspaceId}/finding-exceptions/queue:
|
|
get:
|
|
summary: Canonical exception approval queue
|
|
operationId: listCanonicalFindingExceptionQueue
|
|
tags: [Finding Exceptions]
|
|
parameters:
|
|
- $ref: '#/components/parameters/WorkspaceId'
|
|
- $ref: '#/components/parameters/StateFilter'
|
|
- $ref: '#/components/parameters/DueFilter'
|
|
- name: tenantId
|
|
in: query
|
|
schema:
|
|
type: integer
|
|
responses:
|
|
'200':
|
|
description: Canonical queue filtered to entitled tenants
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
properties:
|
|
data:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/FindingExceptionResource'
|
|
'403':
|
|
$ref: '#/components/responses/Forbidden'
|
|
'404':
|
|
$ref: '#/components/responses/NotFound'
|
|
components:
|
|
parameters:
|
|
TenantId:
|
|
name: tenantId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
FindingId:
|
|
name: findingId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
ExceptionId:
|
|
name: exceptionId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
WorkspaceId:
|
|
name: workspaceId
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: integer
|
|
StateFilter:
|
|
name: state
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [pending, active, expiring, expired, rejected, revoked, superseded]
|
|
DueFilter:
|
|
name: due
|
|
in: query
|
|
schema:
|
|
type: string
|
|
enum: [all, expiring, expired]
|
|
responses:
|
|
Forbidden:
|
|
description: Member lacks required capability
|
|
NotFound:
|
|
description: Workspace or tenant scope is not entitled
|
|
ValidationError:
|
|
description: Input or transition validation failed
|
|
schemas:
|
|
FindingExceptionResource:
|
|
type: object
|
|
required:
|
|
- id
|
|
- tenant_id
|
|
- finding_id
|
|
- status
|
|
- validity_state
|
|
properties:
|
|
id:
|
|
type: integer
|
|
tenant_id:
|
|
type: integer
|
|
finding_id:
|
|
type: integer
|
|
status:
|
|
type: string
|
|
enum: [pending, active, expiring, expired, rejected, revoked, superseded]
|
|
validity_state:
|
|
type: string
|
|
enum: [valid, expiring, expired, revoked, rejected, missing_support]
|
|
owner_user_id:
|
|
type: integer
|
|
nullable: true
|
|
requested_by_user_id:
|
|
type: integer
|
|
approved_by_user_id:
|
|
type: integer
|
|
nullable: true
|
|
requested_at:
|
|
type: string
|
|
format: date-time
|
|
approved_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
review_due_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
FindingExceptionDetail:
|
|
allOf:
|
|
- $ref: '#/components/schemas/FindingExceptionResource'
|
|
- type: object
|
|
properties:
|
|
request_reason:
|
|
type: string
|
|
decisions:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/FindingExceptionDecision'
|
|
evidence_references:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/FindingExceptionEvidenceReference'
|
|
FindingExceptionDecision:
|
|
type: object
|
|
required:
|
|
- decision_type
|
|
- actor_user_id
|
|
- decided_at
|
|
properties:
|
|
decision_type:
|
|
type: string
|
|
enum: [requested, approved, rejected, renewal_requested, renewed, revoked]
|
|
actor_user_id:
|
|
type: integer
|
|
reason:
|
|
type: string
|
|
nullable: true
|
|
effective_from:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
decided_at:
|
|
type: string
|
|
format: date-time
|
|
FindingExceptionEvidenceReference:
|
|
type: object
|
|
required:
|
|
- source_type
|
|
- label
|
|
properties:
|
|
source_type:
|
|
type: string
|
|
source_id:
|
|
type: string
|
|
nullable: true
|
|
source_fingerprint:
|
|
type: string
|
|
nullable: true
|
|
label:
|
|
type: string
|
|
measured_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
summary_payload:
|
|
type: object
|
|
additionalProperties: true
|
|
nullable: true
|
|
RequestExceptionInput:
|
|
type: object
|
|
required:
|
|
- owner_user_id
|
|
- request_reason
|
|
- review_due_at
|
|
properties:
|
|
owner_user_id:
|
|
type: integer
|
|
request_reason:
|
|
type: string
|
|
maxLength: 2000
|
|
review_due_at:
|
|
type: string
|
|
format: date-time
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
evidence_references:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/FindingExceptionEvidenceReference'
|
|
ApproveExceptionInput:
|
|
type: object
|
|
required:
|
|
- effective_from
|
|
- expires_at
|
|
properties:
|
|
effective_from:
|
|
type: string
|
|
format: date-time
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
approval_reason:
|
|
type: string
|
|
nullable: true
|
|
maxLength: 2000
|
|
RejectExceptionInput:
|
|
type: object
|
|
required:
|
|
- rejection_reason
|
|
properties:
|
|
rejection_reason:
|
|
type: string
|
|
maxLength: 2000
|
|
RenewExceptionInput:
|
|
type: object
|
|
required:
|
|
- request_reason
|
|
- review_due_at
|
|
properties:
|
|
request_reason:
|
|
type: string
|
|
maxLength: 2000
|
|
review_due_at:
|
|
type: string
|
|
format: date-time
|
|
expires_at:
|
|
type: string
|
|
format: date-time
|
|
nullable: true
|
|
evidence_references:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/FindingExceptionEvidenceReference'
|
|
RevokeExceptionInput:
|
|
type: object
|
|
required:
|
|
- revocation_reason
|
|
properties:
|
|
revocation_reason:
|
|
type: string
|
|
maxLength: 2000 |