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).


Overview

This reference explains how Pipeline Builder produces, secures, and queries its audit trail, and catalogs every action it records. It’s for compliance reviewers and operators. It covers the emitter paths (platform-direct writes, the service-remote POST /audit/events ingest, and the registry’s Loki structured logs), the per-tenant SHA-256 hash-chain integrity model, sensitive-data scrubbing, and the full action catalog — platform-emitted lifecycle events plus the REMOTE_AUDIT_ACTIONS subset (including billing subscription, tier, addon, and discount actions). The catalog stays in sync with the AuditAction union in code; see Adding a new audit event to extend it.


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, billing.addon.prune, billing.discount.generate, billing.discount.issue, billing.discount.apply, billing.discount.remove, billing.discount.revoke, billing.credit.consumed, billing.credit.exhausted, billing.combo.expired (mirrored to the central trail alongside the service-local billing_events; details carry plan/tier/addon/discount/combo ids + cents only — never payment secrets, coupon tokens, or signing keys)
(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.