feat(cloud-tests): auditor visibility improvements (phases 1-5)#2838
feat(cloud-tests): auditor visibility improvements (phases 1-5)#2838github-actions[bot] wants to merge 7 commits into
Conversation
Adds the depth and audit trail Chris asked for so auditors can trust the automated checks. Five phases shipped together: Phase 1 — Quick wins - Evidence JSON viewer on each finding (sensitive keys redacted via evidence-sanitizer.ts — suffix-match strategy preserves booleans/numbers) - Last-scan metadata strip (47 checks, 41 passed, 6 failed, 3.2s) - Resource labels on every finding row (IAM User: john, S3 Bucket: …) - Rename "Security Findings" header + tab to "Scan Results" Phase 2 — AI check descriptions - Tier 3 panel (what / pass / fail / why) generated lazily on first expand - Claude Haiku 4.5 via @ai-sdk/anthropic, cached per (orgId, checkId) with source-hash invalidation (regenerates when adapter strings change) - Server-side strip of compliance control numbers + URLs as a backstop - GCP/Azure: passthrough from provider catalog (no AI call) Phase 3 — Per-check sub-grouping - Findings within a service grouped by normalized checkKey - "X of Y failing" headers; "Show all results" reveals passing rows - 100-row display cap per check with "Show more" affordance Phase 4 — Comments on auditor findings - CommentsPermissionGuard resolves entityType-specific permission so auditors (finding:update, no task:update) can comment on findings - Comments thread on /overview/findings detail sheet Phase 5 — Resolution + exceptions history - FindingException / FindingResolution / FindingRegression tables - Reconciliation classifies each resolution: platform_fix (RemediationAction matched), external_fix, resource_deleted, or exception_marked - scannedServices column on IntegrationCheckRun prevents false "resolved" events on partial scans - "Mark as exception" modal: required reason (20+ chars), optional reviewer, optional auto-review date - New History tab: summary + resolutions + active exceptions + regressions Includes 96 new tests (59 api + 37 frontend) and 3 Prisma migrations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
11 issues found across 52 files
Confidence score: 2/5
- Merge risk is high because
apps/api/src/comments/comments-permission.guard.tsallows API key/service-token requests without permission-scope checks, which is a concrete authorization bypass on comments endpoints. apps/api/src/cloud-security/evidence-sanitizer.tshas multiple redaction gaps (snake_case keys likeaccess_key_id/secret_access_key, and arrays under sensitive keys), creating a realistic path for secret exposure.- There are also data-consistency/regression risks in
apps/api/src/cloud-security/exception.service.ts,apps/api/src/cloud-security/reconciliation.service.ts, andapps/api/src/cloud-security/cloud-security-query.legacy.ts(duplicate exception/history rows and potentially hidden latest legacy failures). - Pay close attention to
apps/api/src/comments/comments-permission.guard.ts,apps/api/src/cloud-security/evidence-sanitizer.ts, andapps/api/src/cloud-security/exception.service.ts- auth bypass and secret/data-integrity issues are likely to affect production behavior.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/app/src/app/(app)/[orgId]/cloud-tests/components/EvidenceJsonViewer.tsx">
<violation number="1" location="apps/app/src/app/(app)/[orgId]/cloud-tests/components/EvidenceJsonViewer.tsx:68">
P2: The copy button is hover-only visible; add focus visibility so keyboard users can see the focused control.</violation>
</file>
<file name="apps/app/src/app/(app)/[orgId]/cloud-tests/components/MarkExceptionModal.tsx">
<violation number="1" location="apps/app/src/app/(app)/[orgId]/cloud-tests/components/MarkExceptionModal.tsx:163">
P2: The minimum expiration date is computed in UTC, which can incorrectly block valid next-day selections in some time zones.</violation>
</file>
<file name="apps/api/src/cloud-security/evidence-sanitizer.ts">
<violation number="1" location="apps/api/src/cloud-security/evidence-sanitizer.ts:48">
P1: Sensitive key detection misses snake_case credential names, so secrets like `access_key_id`/`secret_access_key` can bypass redaction.</violation>
<violation number="2" location="apps/api/src/cloud-security/evidence-sanitizer.ts:67">
P1: Arrays under sensitive keys are not redacted, which can expose secrets when credentials are represented as string lists.</violation>
</file>
<file name="apps/api/src/cloud-security/exception.service.spec.ts">
<violation number="1" location="apps/api/src/cloud-security/exception.service.spec.ts:197">
P2: The revocation success test does not assert `revokedAt`, so it can pass even if revocation timestamp writing regresses.</violation>
</file>
<file name="apps/api/src/comments/comments-permission.guard.ts">
<violation number="1" location="apps/api/src/comments/comments-permission.guard.ts:68">
P0: API key and service-token requests are granted without any permission scope check, creating an authorization bypass on comments endpoints.</violation>
</file>
<file name="apps/api/src/cloud-security/ai-description.prompt.ts">
<violation number="1" location="apps/api/src/cloud-security/ai-description.prompt.ts:102">
P2: The forbidden-content regexes are too narrow and miss blocked control citations like “CIS 1.8” and “PCI 8.2.3”, allowing policy-violating output to pass server-side validation.</violation>
</file>
<file name="apps/api/src/cloud-security/exception.service.ts">
<violation number="1" location="apps/api/src/cloud-security/exception.service.ts:55">
P1: `markAsException` is non-atomic and can create duplicate active exceptions under concurrent requests.</violation>
</file>
<file name="apps/api/src/cloud-security/cloud-security-query.legacy.ts">
<violation number="1" location="apps/api/src/cloud-security/cloud-security-query.legacy.ts:63">
P1: Filtering with `completedTime <= lastRunTime` can hide the newest legacy failure results when a run writes `IntegrationResult` but does not advance `lastRunAt`.</violation>
</file>
<file name="apps/api/src/cloud-security/cloud-security.controller.ts">
<violation number="1" location="apps/api/src/cloud-security/cloud-security.controller.ts:131">
P2: Date-only exception expirations are parsed as UTC midnight, which can expire exceptions earlier than the user-selected date.</violation>
</file>
<file name="apps/api/src/cloud-security/reconciliation.service.ts">
<violation number="1" location="apps/api/src/cloud-security/reconciliation.service.ts:69">
P1: The idempotency check ignores `FindingRegression`, so rerunning a regression-only run can write duplicate regression history rows.</violation>
</file>
Tip: instead of fixing issues one by one fix them all with cubic
Real bugs flagged by cubic on PR #2838: - comments-permission.guard.ts: API key / service-token requests were granted without any scope check (authorization bypass). Now mirrors the standard PermissionGuard's API-key + service-token enforcement, but with the dynamic `${entityType}:${action}` scope so finding-permission scopes work end-to-end. - evidence-sanitizer.ts: snake_case credential keys (`access_key_id`, `secret_access_key`) and arrays under sensitive keys (e.g. `tokens: [...]`) bypassed redaction. Now normalizes case-separators before matching and redacts string/object elements of arrays under sensitive keys. Added plural variants (tokens / secrets / cookies / accessKeys / signingKeys / sessionTokens / publicKeys / privateKeys / apiKeys / passwords). - exception.service.ts: concurrent markAsException could create duplicate active exceptions. New `@@unique([orgId, connectionId, checkId, resourceId])` constraint + Prisma upsert guarantees a single row per finding atomically. Re-marking after revoke clears `revokedAt` on the same row; full history still lives in AuditLog. - reconciliation.service.ts: idempotency probe only checked FindingResolution — a run that produced only regressions could be re-reconciled and write duplicate regression rows. Now checks both tables. - ai-description.prompt.ts: forbidden-content backstop missed bare control citations like "CIS 1.8" / "PCI 8.2.3" / "NIST AC-2" / "HIPAA 164.312". Added a broader regex catching `(FRAMEWORK) [LETTERS]?[- ]?N(.N)*`. Smaller cleanups also flagged: - MarkExceptionModal / cloud-security.controller: date-only exception expiry was parsed as UTC midnight, expiring exceptions ~8h before the user-selected local date. Frontend now computes `min` from local date, backend now expands bare `YYYY-MM-DD` to end-of-day UTC. - EvidenceJsonViewer copy button: added focus-visible styles so keyboard users can see the focused control. - exception.service.spec.ts: revocation test now asserts revokedAt is a Date in the expected window (caught the missing assertion). Skipped from cubic's list: cloud-security-query.legacy.ts filter — that code is pre-existing and was not introduced by this PR. Test counts after fixes: 105 (was 96). 4 Prisma migrations now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Pushed Real bugs fixed
Skipped (not introduced by this PR)
New migration
Test counts
|
There was a problem hiding this comment.
2 issues found across 15 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="apps/api/src/cloud-security/evidence-sanitizer.ts">
<violation number="1" location="apps/api/src/cloud-security/evidence-sanitizer.ts:76">
P1: Nested arrays under sensitive keys are returned unchanged, so secret strings inside them can leak.</violation>
</file>
<file name="apps/api/src/cloud-security/cloud-security.controller.ts">
<violation number="1" location="apps/api/src/cloud-security/cloud-security.controller.ts:989">
P2: Validate `YYYY-MM-DD` components before calling `Date.UTC`; invalid calendar dates are currently rolled over to a different day instead of being rejected.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.
…cleanup Two related follow-ups to the cubic review: 1. Legacy result filter (cubic finding #6 — earlier deferred as pre-existing, now fixed since the cwd is right here): `getLegacyFindings` used `completedAt <= lastRunAt && completedAt >= lastRunAt - 10min` to keep only the most-recent scan's IntegrationResult rows. This silently hid the newest legacy failures whenever a scan wrote results but did NOT advance `Integration.lastRunAt` (a known consistency gap on the legacy path). Extracted the filter as a pure `filterToLatestScanResults` helper that takes the LATER of `lastRunAt` and the per-integration max result `completedAt` as the reference point. Robust against either signal being stale. Added 8 unit tests (per-integration scoping, fallback when lastRunAt is null, window exclusion, mixed-age data, future-date edge). 2. Prisma migration cleanup: The previous `finding_exception_unique` migration was created manually (via raw SQL file) because `prisma migrate dev` requires a TTY and couldn't run from this environment. `prisma migrate diff` flagged two small drift issues afterwards: - the manual `CREATE UNIQUE INDEX` left an orphan `FindingException_checkId_resourceId_idx` (now subsumed by the four-column unique key — should be dropped) - the index name didn't match what Prisma's @@unique directive expects (truncated differently at the 63-char Postgres limit) New migration `finding_exception_index_cleanup`: - drops the orphan idx - renames the unique index to Prisma's expected name (`<Model>_<col1>_<col2>..._key`) `prisma migrate status` reports the schema is up to date and `prisma migrate diff --from-config-datasource ... --to-schema ...` returns an empty migration — drift resolved. Test counts: 113 (was 105 — added 8 legacy-filter tests). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Follow-up pushed ( 1. Legacy filter (cubic finding #6)Originally I'd skipped this as pre-existing code — the bug is real, just predates this PR. Since the code is on this branch anyway, fixed it here rather than punting:
|
…erated one You were right — the two manually-written migrations from prior commits (`finding_exception_unique` + `finding_exception_index_cleanup`) should be collapsed into a single migration generated the normal way. What I did: - Rolled back the worktree DB to the pre-manual state (dropped the manual unique index, restored the original `_checkId_resourceId_idx`, removed the two migration rows from `_prisma_migrations`) - Deleted both manual migration directories - Ran `prisma migrate dev` properly (used `expect(1)` to handle Prisma's interactive `Are you sure?` prompt — the CLI refuses to proceed non-interactively when adding a unique constraint that could fail on pre-existing duplicates) - Prisma generated `20260513231103_finding_exception_unique` with the exact SQL it would normally produce: DROP the now-redundant idx and CREATE UNIQUE INDEX with the conventional truncated name End state: one clean migration instead of two manuals. `prisma migrate status` → up to date. `prisma migrate diff --from-config-datasource ... --to-schema ...` returns `This is an empty migration.` → zero drift. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…y validation
Two more cubic findings, both real:
1. evidence-sanitizer.ts (P1): `redactArray` handled string and object
elements but not nested arrays — they fell through `return item`
unchanged. Payloads like `{ secrets: [["s1","s2"], ["s3"]] }` would
leak the inner strings. Now recursively redacts.
2. exception-expiry parsing (P2): bare `YYYY-MM-DD` strings like
"2026-02-30" passed straight into `new Date(Date.UTC(2026, 1, 30))`,
which silently rolled forward to March 2 — an exception meant to
expire Feb 30 would actually expire days later than the user expected.
Extracted to `exception-expiry.utils.ts` with strict component
validation that throws BadRequestException for non-existent calendar
dates (Feb 30, month 13, day 0, Feb 29 in non-leap years, etc.).
Test counts: 122 (was 113 — added 1 nested-array sanitizer test + 8
expiry tests).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Both findings real. Fixed in #1 P1 — nested arrays in sanitizer
#2 P2 — invalid calendar dates silently rolling forward
122 tests passing now (was 113 — added 1 nested-array sanitizer + 8 expiry). |
Summary
finding:updatebut nottask:update) to comment on findings via a dynamic-permission guard on the comments controller.Phase-by-phase
Phase 1 — Quick wins
evidence-sanitizer.tsredacts sensitive keys (suffix-match strategy preservesrequirePassword: true-style config booleans)Ran 5m ago • 47 checks • 41 passed • 6 failed • Duration 3.2sIAM User: john,S3 Bucket: logs-archive)Phase 2 — AI check descriptions
@ai-sdk/anthropic. Cached per(orgId, checkId)in newCheckDefinitiontable with source-hash invalidation.Phase 3 — Per-check sub-grouping
checkKey(iam-no-mfa-john→iam-no-mfa).X of Y failing. "Show all results" reveals passing rows.Phase 4 — Comments on auditor findings + cosmetic sweep
findingtoCommentEntityTypeenum.CommentsPermissionGuardresolves entity-type-specific permission dynamically —finding:updatefor finding comments,task:updatefor task comments, etc. Auditors now unblocked./overview/findingsdetail sheet.Phase 5 — Resolution + exceptions history
FindingException,FindingResolution,FindingRegression;FindingResolutionMethodenum.scannedServices+failedServicescolumns onIntegrationCheckRunprevent false "resolved" events when a service wasn't scanned (rate limit / partial-scan protection).platform_fix— matched a successfulRemediationActionbetween scansexternal_fix— customer fixed it in the provider consoleresource_deleted— resource no longer exists in current scanexception_marked— user marked it as an exception(checkKey, resourceId)fails again.Mark as exceptionmodal: required reason (20+ chars), optional reviewer (free text), optional auto-review date.Architecture notes
getFindingsnow filters out findings under active (non-revoked, non-expired) exceptions by default;includeExceptions=trueopt-out for the History tab.Comment.entityIdis already polymorphic — only the enum and permission guard needed extending.currentRunId.DB migrations (3)
20260513213400_cloud_tests_check_definition— Phase 220260513220233_comment_entity_type_finding— Phase 420260513221148_cloud_tests_phase5_history— Phase 5Test plan
evidence-sanitizer(15) ·ai-description.prompt(10) ·check-definition.service(7) ·comments-permission.guard(8) ·exception.service(10) ·reconciliation.service(9) ·check-groups(11) ·CheckGroupBlock(7) ·CheckDefinitionPanel(5) ·EvidenceJsonViewer(5) ·MarkExceptionModal(4) ·HistoryTab(5)npx tsc --noEmitclean for every file touched in api + appevidencepayloads to refine sanitizer wordlist before broad rolloutKnown limitations / follow-ups
resource_deleted+ new failure pair. Underlying ARN tracking would fix this; deferred.task:update(commentId is opaque without an extra DB lookup). Author-only editing means the practical impact is small.scannedServices— reconciliation falls back to "treat all as scanned" when the array is empty. Once GCP/Azure scans start populating it, partial-scan protection extends to those providers automatically.audit-design-systemskill run on touched frontend files — same@trycompai/ui+lucide-reactpatterns as existing code; full DS migration is separate scope.🤖 Generated with Claude Code