Runbook: Ingestion Pipeline Operations
Overview
This runbook covers CloudForge's current finding-ingestion paths, including:
- Startup finding source selection (
mockvspostgres) - Runtime ingestion through
POST /api/v1/findings/ingest - Deduplication behavior and duplicate handling
- Incremental secgraph side effects after ingest
- Failure modes and operator expectations
Runtime note (April 6, 2026): the public demo still defaults to mock-backed findings unless
FINDINGS_SOURCE=postgresandAEGIS_DATABASE_URLare configured. The admin ingest endpoint is a lightweight acceptance + dedup path. It does not replace a full durable scanner ingestion pipeline by itself.
Process Flow
Prerequisites
-
flyctlauthenticated against the live app org - Admin API token for
POST /api/v1/findings/ingest - Viewer or operator API token for read-side verification
- PostgreSQL access if
FINDINGS_SOURCE=postgres -
jqinstalled for response inspection
Current Runtime Model
There are two distinct ingestion paths:
-
Startup corpus loading
FINDINGS_SOURCE=mockor unset: loads the bundled mock JSON corpusFINDINGS_SOURCE=postgres: loads findings from thefindingstable
-
Admin ingest endpoint
POST /api/v1/findings/ingest- validates a compact finding payload
- computes a SHA-256 dedup key from
(source, source_finding_id, resource_id, account_id) - uses a 24-hour in-memory dedup cache
- triggers best-effort incremental secgraph sync if configured
Important limitation:
- The current admin ingest endpoint does not persist the raw finding into the runtime
findingstable or append it to the in-memory corpus used byGET /api/v1/findings. - Its durable effect today is the downstream secgraph materialization path, not full finding-store persistence.
Runtime Controls
| Setting | Default | Purpose |
|---|---|---|
FINDINGS_SOURCE | mock | Selects startup corpus source (mock or postgres) |
AEGIS_DATABASE_URL | unset | Required for FINDINGS_SOURCE=postgres |
FINDINGS_LOAD_TIMEOUT | 30s | Timeout for loading findings from PostgreSQL |
| Dedup cache TTL | 24h | Duplicate suppression window for /findings/ingest |
Startup Verification
Step 1: Confirm the configured source
fly ssh console -a cloudforge-api -C 'printenv | rg "FINDINGS_SOURCE|AEGIS_DATABASE_URL|FINDINGS_LOAD_TIMEOUT"'
fly logs -a cloudforge-api | rg "Findings loaded from mock JSON|Findings loaded from PostgreSQL|FINDINGS_SOURCE=postgres requires"
Expected:
Findings loaded from mock JSONfor the default demo runtimeFindings loaded from PostgreSQLfor DB-backed runtime
If FINDINGS_SOURCE=postgres is set but the DB is unreachable, startup should fail rather than silently degrade.
Step 2: Verify read-side findings surface
export API_BASE="https://api.cloudforge.lvonguyen.com"
curl -sf "$API_BASE/api/v1/findings/stats" \
-H "Authorization: Bearer $VIEWER_TOKEN" | jq .
curl -sf "$API_BASE/api/v1/findings?page=1&per_page=5" \
-H "Authorization: Bearer $VIEWER_TOKEN" | jq '{count: (.data | length), page, per_page, total}'
Expected:
- stable counts from the selected startup source
- no changes from a standalone
/findings/ingestcall unless the underlying source is updated separately
Runtime Ingest Procedure
Step 1: Submit a finding
Required fields:
sourcesource_finding_idresource_idaccount_id
Severity must be one of:
CRITICALHIGHMEDIUMLOWINFO
curl -s -X POST "$API_BASE/api/v1/findings/ingest" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"source": "aws-securityhub",
"source_finding_id": "SH-001",
"resource_id": "arn:aws:s3:::example-bucket",
"account_id": "123456789012",
"severity": "HIGH",
"finding_type": "public bucket",
"title": "S3 bucket is publicly accessible",
"description": "Public read ACL detected"
}' | jq .
Expected success:
201 Created- response contains
status: "accepted",finding_id,dedup_key
Step 2: Verify duplicate handling
Resubmit the exact same payload within 24 hours.
curl -s -o /tmp/ingest-dup.json -w "%{http_code}\n" \
-X POST "$API_BASE/api/v1/findings/ingest" \
-H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d @/tmp/ingest-payload.json
cat /tmp/ingest-dup.json | jq .
Expected duplicate response:
409 Conflictstatus: "duplicate"existing_entry.finding_idexisting_entry.first_seen
Secgraph Side-Effect Verification
If secgraphSync is configured, a successful ingest attempts a best-effort incremental sync with a 5-second timeout.
Step 1: Check logs
fly logs -a cloudforge-api | rg "finding ingested|duplicate finding rejected|incremental secgraph sync failed after finding ingest"
Expected:
finding ingestedon successduplicate finding rejectedon duplicate payloads- if present,
incremental secgraph sync failed after finding ingestis a degraded warning, not an ingest failure
Step 2: Verify graph-native issue side effects
Because the raw findings list is not updated by this endpoint, verify via the secgraph issue surface instead of GET /findings.
curl -sf "$API_BASE/api/v1/issues?per_page=10" \
-H "Authorization: Bearer $VIEWER_TOKEN" | jq .
curl -sf "$API_BASE/api/v1/issues/stats" \
-H "Authorization: Bearer $VIEWER_TOKEN" | jq .
If the accepted ingest produced a mapped control failure, new issue materialization may appear here even though /api/v1/findings stays unchanged.
Failure Modes
400 Bad Request
Causes:
- missing
source,source_finding_id,resource_id, oraccount_id - invalid severity
- malformed JSON body
Action:
- fix the payload and retry
401 / 403
Cause:
- missing or insufficient auth
Action:
/findings/ingestis admin-only- use a token with admin role claims
409 Conflict
Cause:
- duplicate finding within the 24-hour TTL window
Action:
- treat as expected deduplication, not an incident
- inspect
existing_entryin the response
Startup failure with FINDINGS_SOURCE=postgres
Cause:
AEGIS_DATABASE_URLmissing or unreachable
Action:
fly logs -a cloudforge-api | rg "FINDINGS_SOURCE=postgres requires|query findings|scan finding row|PostgreSQL ping failed"
- restore DB connectivity or revert
FINDINGS_SOURCEtomock
Verification
- Startup logs match the intended source (
mockorpostgres) -
/api/v1/findings/statsreturns200 - Valid ingest payload returns
201 - Duplicate payload returns
409 - If secgraph is enabled, issue/graph surfaces reflect the downstream materialization path
Rollback
If runtime ingestion changes cause instability:
fly secrets set FINDINGS_SOURCE=mock -a cloudforge-api
fly secrets unset AEGIS_DATABASE_URL -a cloudforge-api
Then redeploy or restart the app and confirm startup returns to mock-backed operation.
If only the admin ingest endpoint is problematic, disable access operationally at the API gateway or revoke admin token access until the issue is fixed.
Escalation
| Condition | Action |
|---|---|
DB-backed startup fails with FINDINGS_SOURCE=postgres | Escalate to platform owner immediately |
Ingest returns 201 but expected secgraph side effects never appear | Check secgraph sync warnings, then escalate |
| Duplicate suppression behaves unexpectedly across restarts | Review in-memory dedup TTL assumptions and restart behavior |
Operators assume /findings/ingest is durable raw-finding persistence | Stop rollout and clarify current contract before further automation |