Self-Service CI/CD for AWS

Self-service AWS CodePipeline platform — developers ship compliant CI/CD pipelines in minutes via dashboard, CLI, CDK, or AI prompt, while platform teams enforce policy-as-code guardrails, governance, and per-team isolation.

Audit Events

Pipeline Builder keeps a tamper-evident audit trail in the platform service’s MongoDB audit_events collection. Two emitters feed it, and a separate structured-log path exists for the image registry.

Both emitter paths funnel through one appender (appendAuditEvent in platform/src/helpers/audit-chain.ts), so every stored event is hash-chained and scrubbed the same way.

Query the trail via GET /audit (admin-only; org admins are forced to their own org, sysadmins may filter any org) or the dashboard Audit page at /dashboard/audit. Records auto-expire via a MongoDB TTL index after config.audit.retentionDays days (default 90, overridable via AUDIT_RETENTION_DAYS).


Integrity & tamper-evidence

Every event is linked into a per-tenant SHA-256 hash chain: each row stores a hash over its immutable fields plus the prevHash of the previous event in the same chain (chain key = affectedOrgId ?? orgId). Altering, reordering, or deleting a stored event breaks the chain.

Sensitive-data scrubbing

appendAuditEvent runs scrubAwsIdentifiers() over every event’s details before hashing and storing, redacting AWS-account-id-shaped tokens (including the account segment of any ARN) and account-named keys. An AWS account id is never persistedorgId is the marketplace customerIdentifier, never an AWS account id. Emitters must also keep secrets/tokens out of details; the frontend applies a second redaction pass before rendering or exporting.


Service-remote ingest (POST /audit/events)

Non-platform services deliver events through RemoteAuditClient, which is best-effort and fire-and-forget — a failed audit never blocks or fails the originating mutation. Three properties make it safe and durable:

Observability — audit loss is metered, not just logged: audit_emitted_total, audit_dropped_total, audit_spool_{enqueued,dropped,redelivered}_total.


Action catalog

The full set of platform actions lives in the AuditAction union in platform/src/models/audit-event.ts; the subset a remote service may emit is REMOTE_AUDIT_ACTIONS in packages/api-core/src/services/remote-audit-client.ts.

Platform-emitted

Area Actions
User lifecycle user.register, user.login, user.login.failed, user.logout, user.delete, user.profile.update, user.password.change, user.email.verified, user.token.create, user.tokens.revoke-all
Organization org.create, org.update, org.soft_delete, org.restore, org.switch, org.member.add, org.member.remove, org.member.deactivate, org.member.activate, org.ownership.transfer
Invitations invitation.send, invitation.accept, invitation.revoke, invitation.resend
Permission roles org.role.create, org.role.update, org.role.delete, org.role.member.add, org.role.member.remove
Dashboards & alerts dashboard.create/update/delete/clone, alert.destination.create/update/delete/test, alert.rule.create/update/delete
Admin / sysadmin admin.user.create/update/delete, admin.org.delete, admin.org.export, admin.org-idp.upsert/delete, admin.superadmin.grant/revoke, admin.org.kms-config.upsert/delete, org.kms.orphaned, admin.org.tier.update, admin.org.seatLimit.update, admin.org.quota.override, admin.org.ai-config.update, admin.user.features.update, admin.impersonate.start, admin.org.namespace.render
Denied access authz.denied — emitted by the shared permission gate when a state-changing (non-GET) request is rejected, so probing / privilege-escalation attempts leave a trail (outcome: 'failure')

Each record carries actorId/actorEmail, orgId (the actor’s own org), and affectedOrgId (the org actually operated on). They diverge when a sysadmin acts on another org, so the trail answers “what did a sysadmin do to org X?” — SOC2 evidence for impersonation-style access. admin.* actions and admin.impersonate.start set affectedOrgId to the target org so the affected org’s own admins can see them.

Service-emitted (REMOTE_AUDIT_ACTIONS)

Service Actions
Plugin plugin.build.completed, plugin.build.failed, plugin.build.timeout, plugin.delete, plugin.upload, plugin.deploy, plugin.bulk.update, plugin.bulk.delete, plugin.dlq.purge
Pipeline pipeline.create, pipeline.update, pipeline.delete, pipeline.execution.start, pipeline.execution.cancel, pipeline.registry.register, pipeline.registry.deregister
Quota quota.reset, quota.limit.update, quota.delete
Compliance compliance.exemption.approve, compliance.exemption.revoke, compliance.rule.toggle, compliance.rule.create/update/delete, compliance.policy.create/update/delete, compliance.scan-schedule.create/update/delete, compliance.template.apply, compliance.scan.cancel
Image registry registry.gc, registry.image.delete
Message message.announcement.create, message.delete (admin broadcasts + deletes only — 1:1 messages are not audited, and no message body reaches details)
Billing billing.subscription.cancel, billing.subscription.delete, billing.tier.override, billing.addon.add, billing.addon.remove (mirrored to the central trail alongside the service-local billing_events; details carry plan/tier/addon ids only — never payment secrets)
(all services) authz.denied

Plugin build terminal outcomeplugin.build.failed / plugin.build.timeout is emitted at TRUE dead-letter-queue exhaustion, not at the tier queue’s final attempt. A job that fails the tier queue but later succeeds in the DLQ emits only plugin.build.completed — the trail records exactly one terminal outcome per build, never a “failed” that a later “completed” contradicts.


Registry structured-log events

Independently of the Mongo trail, image-registry emits eventCategory: 'audit' structured log lines (via emitAudit in packages/api-core/src/utils/audit.ts) that the log aggregator (Loki, in the default deploy) routes into a dedicated stream. The event-name union is packages/api-core/src/types/audit-events.ts.

Querying

Audit log lines land in Loki with service_name, eventCategory, event, actor, and pluginName promoted to labels. From the UI, the Audit Activity dashboard at /dashboard/observability/audit-activity is the operator-facing surface; deep-link to a filtered view via the registry’s buildAuditLogLink helper (frontend/src/lib/registry-audit-link.ts).

Direct LogQL (hitting Loki at port 3100):

{service_name="pipeline-image-registry", eventCategory="audit", event="registry.tag.copy"}
  | json
  | isPromotionToSystem=`true`

registry.tag.copy

Emitted by image-registry’s POST /api/images/copy after a successful cross-repo tag copy.

Field Type Description
event 'registry.tag.copy' Constant discriminator
actor string req.user.sub of the sysadmin who initiated the copy
source string Source <repo>:<ref>
target string Target <repo>:<ref>
sourceDigest string Resolved digest of the source manifest
targetDigest string Resolved digest of the target manifest
isPromotionToSystem boolean true when the target repo starts with system/ — the highest-privilege case
mounted.manifests number Total manifests PUT
mounted.blobs number Count of UNIQUE blob digests cross-mounted

Why isPromotionToSystem matters: copying any tag into system/* makes the image visible to every authenticated user. Operators should alert on these specifically — they’re meaningful trust escalations.

registry.tag.delete

Emitted by image-registry’s DELETE /api/images/{name}/manifests/{reference} after a successful delete.

Field Type Description
event 'registry.tag.delete' Constant discriminator
actor string req.user.sub of the sysadmin who initiated the delete
repo string Repository name (e.g. org-acme/foo)
ref string Tag or digest the operator passed in
digest string Resolved manifest digest that was actually deleted

Adding a new audit event

Platform-emitted (user/org lifecycle):

  1. Add the action to the AuditAction union AND the ALL_AUDIT_ACTIONS array in platform/src/models/audit-event.ts.
  2. Call audit(req, 'new.action', { targetType, targetId, affectedOrgId, details }) from the controller after the mutation succeeds. Keep secrets / tokens / AWS account ids out of details.
  3. Document it in the action catalog above.

Service-emitted (a non-platform service):

  1. Add the action to REMOTE_AUDIT_ACTIONS in remote-audit-client.ts AND to the platform AuditAction union / ALL_AUDIT_ACTIONS (the subset-guard test enforces REMOTE_AUDIT_ACTIONS ⊆ AuditAction).
  2. Emit it via the service’s getAuditClient().record({ action, actorId, orgId, targetId, details }, '<service>') after the mutation succeeds.
  3. Document it in the service-emitted catalog above.

Use the dot-separated <area>.<entity>.<verb> naming convention so events sort and filter cleanly.