Claude/domain records 1van5c - #65
Conversation
…dashboard - Next.js 14 app with TypeScript, Tailwind, dark ocean theme - Prisma schema: Customer, Domain, ScanRun, Finding, ReputationCheck, DnsRecordSnapshot - Real DNS scanners: SPF (multi-record, lookup count), DMARC, DKIM (13 selectors), DNSSEC, CAA - Reputation module: Spamhaus DBL (live), Google Safe Browsing/VirusTotal/Talos/URLScan/SmartScreen (placeholders) - Scoring engine: starts at 100, deductions per spec, posture: healthy/watch/needs_improvement/poor - Dashboard, Customers, Domains, Findings, Reputation, Settings pages - API routes: GET/POST /api/domains, /api/domains/[id], /api/scan/domain, /api/scan/all, /api/findings, PATCH /api/findings/[id] - Slack alert abstractions: score drop, poor posture, DMARC disappear, SPF invalid, blacklist hit - Scanner worker: npm run scan:all (tsx scripts/scan-all.ts) - Seed data: Spinato's Pizza (score 35, poor) and Atlas Healthcare Partners (score 65, needs_improvement) - DigitalOcean App Platform config: .do/app.yaml with web service + hourly scheduled scanner job - GitHub Actions CI: lint, tsc, build Co-Authored-By: Claude Sonnet 4.6 <[email protected]> Claude-Session: https://claude.ai/code/session_01RMiVydSVDjTrqBCeG8k8PJ
…schema Co-Authored-By: Claude Sonnet 4.6 <[email protected]> Claude-Session: https://claude.ai/code/session_01RMiVydSVDjTrqBCeG8k8PJ
Co-Authored-By: Claude Sonnet 4.6 <[email protected]> Claude-Session: https://claude.ai/code/session_01RMiVydSVDjTrqBCeG8k8PJ
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fd48ad2a1a
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| repo: chelstein/taste-skill | ||
| branch: main | ||
| deploy_on_push: true | ||
| build_command: npm install && npx prisma generate && npm run build |
There was a problem hiding this comment.
Apply the Prisma schema during deploy
In the DigitalOcean deploy path this provisions a fresh Postgres database, but the service build only runs prisma generate and next build (and the repo contains no checked-in prisma/migrations). prisma generate does not create tables, so a first App Platform deploy starts the web service and scanner against an empty database, causing runtime queries/scans to fail until someone manually runs db push/migrations. Add a migration/db-push step before startup or check in migrations and run prisma migrate deploy.
Useful? React with 👍 / 👎.
| const hardfail = primary.includes('-all'); | ||
|
|
||
| // Count DNS-lookup mechanisms (a, mx, include, exists, redirect) | ||
| const lookupMechanisms = (primary.match(/\b(include:|a:|mx:|exists:|redirect=)/gi) ?? []).length; |
There was a problem hiding this comment.
Count all SPF lookup mechanisms
For SPF records that use bare a or mx mechanisms, such as v=spf1 a mx include:... -all, this regex only counts a: and mx: forms, so records can exceed SPF's 10-DNS-lookup limit while tooManyLookups stays false and no finding/deduction is generated. Parse/count bare a/mx (and nested include/redirect lookups) before scoring this check.
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| const get = (tag: string) => { | ||
| const m = flat.match(new RegExp(`${tag}=([^;]+)`, 'i')); |
There was a problem hiding this comment.
Anchor DMARC tag parsing to tag boundaries
When a DMARC record contains sp= but omits p=, get('p') matches the p= inside sp= and treats an invalid record as if it had a domain policy. That lets malformed DMARC avoid the missing/weak-policy deduction and be stored as a valid policy; split on semicolons or require a start/semicolon boundary for each tag name.
Useful? React with 👍 / 👎.
| if (!webhookUrl) return; | ||
|
|
||
| const color = payload.severity === 'critical' ? '#FF0000' : payload.severity === 'warning' ? '#FFA500' : '#0099FF'; | ||
|
|
There was a problem hiding this comment.
When SLACK_WEBHOOK_URL is set, a network error or invalid webhook URL makes this fetch reject, and /api/scan/domain awaits alerts before refreshing findings. In that environment a transient Slack failure turns an otherwise successful scan into a 500 and leaves findings stale or partially updated; catch/log alert delivery errors or move them out of the critical scan persistence path.
Useful? React with 👍 / 👎.
Co-Authored-By: Claude Sonnet 4.6 <[email protected]> Claude-Session: https://claude.ai/code/session_01RMiVydSVDjTrqBCeG8k8PJ
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 28c474e22e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const dblRecords = await resolve4(`${domain}.dbl.spamhaus.org`).catch(() => null); | ||
| results.push({ | ||
| name: 'Spamhaus DBL', | ||
| listed: !!dblRecords, |
There was a problem hiding this comment.
Don't treat Spamhaus error codes as listings
When the scan runs through a public/open resolver or hits Spamhaus query limits, resolve4 can return 127.255.255.254/255; Spamhaus documents these 127.255.255.0/24 answers as query errors, not reputation data (Spamhaus guidance). Because any non-empty response sets listed: true, clean domains in those environments get critical blacklist findings, 30-point deductions, and Slack alerts; filter valid DBL listing codes and surface 127.255.255.* as error/unknown instead.
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| export async function checkDmarc(domain: string): Promise<DmarcResult> { | ||
| const records = await getTxtRecords(`_dmarc.${domain}`); |
There was a problem hiding this comment.
Reject duplicate DMARC records
When DNS returns two _dmarc TXT records that start with v=DMARC1, this find silently chooses one and treats the domain as protected. RFC 7489 policy discovery terminates when multiple DMARC records remain (section 6.6.3), so common migration overlaps can leave receivers with no applicable DMARC policy while the scanner skips the missing/invalid-policy finding; count matching records and report duplicates as invalid instead of picking one.
Useful? React with 👍 / 👎.
| } | ||
|
|
||
| // Upsert findings | ||
| await prisma.finding.deleteMany({ where: { domainId } }); |
There was a problem hiding this comment.
Preserve resolved finding state on rescan
The app exposes PATCH /api/findings/[id] to change a finding's status, but every rescan deletes all findings for the domain and recreates current deductions with the default open status (the scheduled worker repeats the same pattern in scripts/scan-all.ts). If an operator marks a finding resolved or suppressed while the DNS fix is still pending, the next scan loses that state and reopens it; delete only scanner-owned open findings or upsert by stable key while preserving status/history.
Useful? React with 👍 / 👎.
No description provided.