diff --git a/CHANGELOG.md b/CHANGELOG.md index 55b4eed..7bce3ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,9 @@ # Changelog -## Unreleased +## 0.3.0 (9 November 2026) + +### Accuracy +- First blind, separately scored run: `examples/kadamba-fit.run-1` holds the report and score. 16 of 16 planted issues found, 3 of 13 should-not-flag items flagged. The README's "How accurate is it?" section has the caveats. ### Skills Changes from the first real run of `/onboard` and `/audit`, on `examples/acme-demo`. diff --git a/CITATION.cff b/CITATION.cff index 5cf7607..1d28df8 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -6,8 +6,8 @@ authors: - name: "Vigil contributors" repository-code: "https://github.com/prathikcannotcode/Vigil" license: Apache-2.0 -version: 0.2.0 -date-released: 2026-11-02 +version: 0.3.0 +date-released: 2026-11-09 keywords: - dpdp - gdpr diff --git a/README.md b/README.md index 85014b3..fa587d3 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,20 @@ That picture is **sample-co**, a made-up lending app. Its files were written by - [`report.json`](assessments/2026-10-19-sample-co-baseline/report.json): 9 schema-valid findings with owners, due dates and cross-references between DPDP and GDPR - [`breach-log.jsonl`](company-profile/sample-co/breach-log.jsonl): an incident from detection to CERT-In (5h 25m) and the EU authority (44h 10m), one append-only line per step -To watch a real run, audit [`examples/acme-demo`](examples/acme-demo), a small synthetic app with planted problems, and score the result against [the list of what it should find](examples/acme-demo.expected.json). Quick start step 5 shows how. That list is also the place to start if you want to measure Vigil's accuracy; no benchmark results are published yet. +To watch a real run, audit [`examples/acme-demo`](examples/acme-demo), a small synthetic app with planted problems, and score the result against [the list of what it should find](examples/acme-demo.expected.json). Quick start step 5 shows how. + +## How accurate is it? + +One blind run so far, on [`examples/kadamba-fit`](examples/kadamba-fit): a 55-file synthetic fitness app whose code states none of its gaps. One session ran `/audit` without seeing the answer list, and a separate session scored it. The [score](examples/kadamba-fit.run-1/score.md): + +| Measure | Result | +|---|---| +| Planted issues found | 16 of 16 | +| Citation anchors cited exactly | 45 of 48 | +| Should-not-flag items flagged anyway | 3 of 13; two were mistakes in the answer list, since corrected | +| Fixture values copied into any output | 0 | + +Treat that as a floor check, not a benchmark. It is one run on synthetic code, and Claude wrote the app, ran the audit and scored it. The answer list is now public, so a fair re-run copies the app out of the repo and never opens [`examples/kadamba-fit.expected.json`](examples/kadamba-fit.expected.json) or the [run sheet](examples/kadamba-fit.run-sheet.md). ## Why Vigil @@ -188,7 +201,7 @@ Skills are Markdown that tell Claude what the law requires and how to check for | Schema validation | `validate-on-write.mjs`, `npm run validate` | A finding without a clause or severity, a breach entry without an event, a posture with an unknown status. 9 schemas. | | Secrets | `write-guard.mjs` | AWS, GitHub, OpenAI and Anthropic-style keys, private keys, password assignments | | No personal data in findings | `write-guard.mjs`, skill rules, schema shape | Aadhaar, PAN and +91 mobile numbers in company or assessment data. Findings carry `file` and `line`, never the value found there. Other identifiers rest on the skill rules. | -| Tests | `npm test`, GitHub Actions | Regressions in all of the above. 23 tests, Node's built-in runner. | +| Tests | `npm test`, GitHub Actions | Regressions in all of the above. 24 tests, Node's built-in runner. | Known limit: hooks guard Claude Code's Write, Edit and MultiEdit tools. A shell command run through Bash is not intercepted, so `.claude/settings.json` only pre-approves the validate and test commands. Details in [SECURITY.md](SECURITY.md). diff --git a/examples/kadamba-fit.run-1/report.json b/examples/kadamba-fit.run-1/report.json new file mode 100644 index 0000000..4cb187b --- /dev/null +++ b/examples/kadamba-fit.run-1/report.json @@ -0,0 +1,534 @@ +{ + "assessment_id": "2026-11-05-kadamba-cold-full", + "company_id": "kadamba-cold", + "scope": ["DPDP", "GDPR"], + "started_at": "2026-11-05T17:02:00Z", + "completed_at": "2026-11-05T17:30:00Z", + "assessor": "Vigil on Claude Code", + "target": { + "repo": "prathikcannotcode/Vigil, examples/kadamba-fit on branch claude/trusting-wilbur-d90e22, audited from an extracted copy", + "commit": "b6d1c441c798009345e65be717f6c8373fa77b8e" + }, + "model": "claude-opus-5-5", + "vigil_version": "0.2.0", + "law_as_at": "2026-11-02", + "coverage": { + "applicable": 51, + "examined": 45 + }, + "findings": [ + { + "id": "F-0001", + "severity": "critical", + "framework": "GDPR", + "clause": "Art. 5(1)(c), Art. 9(1)", + "cross_reference": "DPDP s.6(1), Rule 3(b)", + "issue": "The weekly Harbourline export sends far more than members agreed to. The query selects email, date of birth, region, step total, resting heart rate and average sleep alongside the tier (lines 8-9), and json=dict(row) posts every column (line 20). Partner agreement Schedule A (docs/vendors/harbourline-partner-agreement.md:11-13), the signup text (web/signup.html:14) and the notice (web/privacy-notice.html:17) all limit the share to member_ref, week_start and activity_tier. Harbourline is an insurer and an independent controller, so health data and a direct identifier reach a party that prices policies, and the email undoes the opaque member_ref. The job checks consent only: it checks no policy link code and the schema has no column for one, so members without a Harbourline policy are sent too. GDPR applies today. DPDP readiness gap: s.6 and Rule 3 apply from 13 May 2027.", + "recommendation": "Stop the job. Select only member_ref, week_start and activity_tier and build the payload from an explicit allow-list. Send only members who have linked a policy. Ask Harbourline to delete the excess fields already received, and assess the past exports as a personal data breach under Art. 33 and s.8(6).", + "file": "jobs/partner_export.py", + "line": 8, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0002", + "severity": "critical", + "framework": "GDPR", + "clause": "Art. 5(1)(e), Art. 17(1)", + "cross_reference": "DPDP s.8(7), s.12(3), Rule 8", + "issue": "Health and cycle data survive account closure and erasure requests. ERASE lists only bookings, route_tracks, workouts, auth_sessions and payment_tokens, and the loop deletes by member_id. health_samples, weekly_summaries and cycle_logs are keyed by member_ref (db/migrations/0004_wearable.sql) and no other job deletes them. The member row goes and these rows stay indefinitely, with the device serial and member_ref. The retention schedule (docs/retention-schedule.md:8) and the notice (web/privacy-notice.html:26) both promise deletion 30 days after closure, and DELETE /me calls the same function (services/rights.py:24), so an erasure request leaves them too. GDPR applies today. DPDP readiness gap: these clauses apply from 13 May 2027.", + "recommendation": "Add the three member_ref tables to erase_member, delete existing rows whose member_ref is no longer in members, and add a test that fails when a table holding member_id or member_ref is neither purged nor listed as retained in the schedule.", + "file": "jobs/purge_closed.py", + "line": 6, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0003", + "severity": "critical", + "framework": "GDPR", + "clause": "Art. 9(1), Art. 9(2)(a)", + "cross_reference": "DPDP s.4, s.6(1), s.7(a)", + "issue": "No Art. 9(2) condition covers health data. The RoPA (lines 9-10) and the notice (web/privacy-notice.html:14-15) rely on contract for heart rate, SpO2, sleep and cycle data, and contract is not an Art. 9(2) condition. No explicit consent is collected: config/app.yaml:7-11 has no wearable or cycle purpose, and services/wearable_sync.py:6 and services/cycle.py:6 check nothing before writing. The wellness row (line 17) cites Art. 6(1)(a) with no Art. 9(2)(a), although the tier sits in weekly_summaries beside heart rate and sleep averages and no code in the repository shows how it is derived. GDPR applies today. DPDP readiness gap: DPDP has no contract ground, so from 13 May 2027 these flows need s.6 consent unless s.7(a) (data voluntarily provided for a specified purpose) covers them. That point needs a legal view.", + "recommendation": "Add separate explicit-consent purposes for wearable sync and cycle tracking, gate ingest on them, and record the Art. 9(2)(a) condition in the RoPA and notice. Get counsel's view on the s.7(a) route for India members and on whether the tier is data concerning health.", + "file": "docs/ropa.md", + "line": 9, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0004", + "severity": "critical", + "framework": "GDPR", + "clause": "Art. 32(1)(b), Art. 5(1)(f), Art. 25(2)", + "cross_reference": "DPDP s.8(5), Rule 6(1)(a), Rule 6(1)(b)", + "issue": "Production data reaches staging unmasked. The weekly refresh dumps each production database and restores it into staging, excluding only payment_tokens and coach_payouts (lines 6-10). scripts/mask_staging.sql exists but nothing calls it: neither this script nor deploy/staging-access.yaml references it. All of engineering and the qa-contractors group have read-write access to staging (deploy/staging-access.yaml:3-5), so they can read names, contacts, dates of birth, GPS routes, heart rate, cycle logs, children's records and support tickets for both regions. The RoPA has no entry for this use. The dump also stays in /tmp if pg_restore fails, because set -e exits before the rm on line 11. GDPR applies today. DPDP readiness gap: these clauses apply from 13 May 2027.", + "recommendation": "Stop the refresh and purge the current staging copies. Seed staging with synthetic data, or run a complete masking step inside the pipeline and fail the job if it does not run. Limit access to named people with a need, confirm where the contractors sit (a transfer question for EU data), and remove the dump with a trap on exit.", + "file": "scripts/refresh_staging.sh", + "line": 6, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0005", + "severity": "critical", + "framework": "GDPR", + "clause": "Art. 32(1)(a), Art. 5(1)(c)", + "cross_reference": "DPDP s.8(5), Rule 6(1)(a)", + "issue": "Production logs hold full request headers and bodies. RequestLogMiddleware logs dict(request.headers) and request.body at DEBUG, and config/production.yaml:20 sets kadamba.api.middleware to DEBUG while everything else is INFO. So production writes session credentials in headers, OTP codes (POST /otp/verify), signup details, wearable samples (POST /me/wearable/samples), cycle logs and notes (POST /me/cycles), nominee details and ticket text into Loki, where deploy/logging.yaml keeps them for 720 hours. This defeats the phone masking in services/otp.py. GDPR applies today. DPDP readiness gap: Rule 6(1)(a) applies from 13 May 2027.", + "recommendation": "Set the logger to INFO in production now, remove headers and body from the log call, purge the api-app log data, rotate any session tokens that were logged, and review who has read those logs to decide whether a breach assessment is needed.", + "file": "api/middleware.py", + "line": 11, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0006", + "severity": "critical", + "framework": "GDPR", + "clause": "Art. 32(1)(b)", + "cross_reference": "DPDP s.8(5), Rule 6(1)(b)", + "issue": "A production database URL with its password is committed in a Kubernetes Secret. Base64 is encoding, so anyone with read access to the repository can use it against the production database that holds member health, cycle and children's data. The value carries a synthetic marker and the README says all credentials are made up. That claim was not verified, and committing production secrets is a control gap either way. GDPR applies today. DPDP readiness gap: Rule 6(1)(b) applies from 13 May 2027.", + "recommendation": "Rotate the credential, remove the file from the repository and its history, and deliver secrets through a secret manager or sealed secrets. Add secret scanning to CI. config/env.example holds only placeholders and needs no change.", + "file": "deploy/secrets.yaml", + "line": 7, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0007", + "severity": "critical", + "framework": "GDPR", + "clause": "Art. 44, Art. 46(1), Art. 13(1)(f)", + "issue": "EU member data goes to the US with no transfer mechanism on file. config/production.yaml:7 routes EU analytics to a us-east-1 endpoint, and services/analytics.py:7-12 sends member ref, email and IP address. The Quantlytics order form has no SCCs, no Data Privacy Framework reference and no transfer impact assessment. The vendor register (docs/vendors/register.md:7) says 'See contract' for the transfer tool, and the contract has none. The notice (web/privacy-notice.html:23) tells members that standard contractual clauses cover anyone outside the EU, which does not hold for this flow. Kadamba Fit Ireland Limited, the controller for EU members, is not a party to the order form. This applies today.", + "recommendation": "Stop EU events to us-east-1, or repoint them to an EU endpoint if Quantlytics offers one. If the US flow stays, the Irish entity signs a DPA with the 2021 SCCs and a transfer impact assessment, or confirms Quantlytics' DPF certification and keeps SCCs as the fallback while C-703/25 P is pending. Correct the notice and register. Past transfers have no mechanism, so take the DPO's and counsel's advice on them.", + "file": "config/production.yaml", + "line": 7, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0008", + "severity": "critical", + "framework": "DPDP", + "clause": "s.9(3)", + "cross_reference": "GDPR Art. 5(1)(a), Art. 25(1)", + "issue": "DPDP readiness gap: s.9 applies from 13 May 2027, and the same code runs for EU children today. Children on a family plan can receive targeted sponsored offers. services/offers.py:7 returns nothing only when member.is_minor is set, but add_dependant creates the member without is_minor (lines 6-11) and db/migrations/0001_members.sql:10 defaults it to FALSE. Every dependant passes the check and gets offers ranked on 90 days of workouts. The guard works only for EU 16 and 17 year olds who signed up themselves (api/signup.py:29). The Member model is not in the repository; signup passes the flag explicitly, and nothing shown sets it for dependants.", + "recommendation": "Set is_minor in add_dependant and backfill, or derive minor status from dob and guardian_id when it is read. Block offers, segmentation and analytics for anyone under 18, and add a test using a dependant.", + "file": "services/family.py", + "line": 6, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0009", + "severity": "critical", + "framework": "DPDP", + "clause": "s.9(1), Rule 10", + "cross_reference": "GDPR Art. 8(2), Art. 9(2)(a), Art. 7(1)", + "issue": "DPDP readiness gap: s.9 and Rule 10 apply from 13 May 2027; GDPR Art. 8 applies today. add_dependant records a child with no verifiable parental consent. It takes a name and date of birth from any logged-in member. No Rule 10 identity check or token exists, nothing confirms the dependant is a child or this member's child, and the adult's age is only the date of birth they typed at signup. The EU minimum of 16 lets a 16 year old add a child. Region is self-selected at signup (web/signup.html:2-5), so someone under 18 in India can pick the EU route, and dob can be retried or changed later with no re-check (api/signup.py:16-18, services/rights.py:18). No consent_events row is written, so there is no proof under s.6(10) or Art. 7(1). A dependant is then an ordinary member record: the wearable and cycle services have no minor or guardian check (services/wearable_sync.py:6, services/cycle.py:6).", + "recommendation": "Build a parental consent step: verify the adult with reliable identity and age details or a virtual token from an authorised entity such as DigiLocker, confirm the parent is an adult, write the consent with its notice version to consent_events, and hold back cycle tracking and wearable sync for a child until it exists. Record any Fourth Schedule exemption relied on.", + "file": "services/family.py", + "line": 4, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0010", + "severity": "high", + "framework": "GDPR", + "clause": "Art. 7(4), Art. 4(11), Art. 9(2)(a)", + "cross_reference": "DPDP s.6(1); CCPA Dark Patterns Guidelines 2023 (forced action)", + "issue": "Signup refuses an account unless the member agrees to share with the insurer. config/app.yaml:14 lists wellness_partner as required, and api/signup.py:19-21 rejects the form with a generic message to tick the boxes. The form (web/signup.html:14) shows it as an ordinary box, the notice says 'if you agree', and the feature is useful only to Harbourline policyholders. EU members aged 16 and 17 are forced too. Consent made a condition of a service that does not need it is not free, so the basis for the Harbourline flow fails in the EU today, and this is a forced-action dark pattern. Withdrawal in Settings after signup does not cure the condition. DPDP readiness gap: s.6(1) applies from 13 May 2027.", + "recommendation": "Remove wellness_partner from required_purposes and keep only terms required. Re-collect valid explicit consent from existing members and suspend sharing for anyone whose only consent is the forced one.", + "file": "config/app.yaml", + "line": 14, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0011", + "severity": "high", + "framework": "GDPR", + "clause": "Art. 7(3), Art. 21(3)", + "cross_reference": "DPDP s.6(4), s.6(6)", + "issue": "Withdrawing marketing consent does not stop marketing. Settings calls api/consents.py:10, which writes only to consent_events. This job selects on members.marketing_opt_in, a column set once at signup (api/signup.py:30) and never updated. A withdrawn member stays in the nightly sync, and the job only upserts, so nothing removes a contact from the Cadencehive offers list. The reminder and partner jobs read the ledger correctly; this one does not. GDPR applies today. DPDP readiness gap: s.6 applies from 13 May 2027.", + "recommendation": "Read consent_ledger.current(member.ref, 'marketing') in campaign_sync, remove contacts whose consent is withdrawn, drop the marketing_opt_in column, and reconcile the current Cadencehive lists against the ledger.", + "file": "jobs/campaign_sync.py", + "line": 6, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0012", + "severity": "high", + "framework": "GDPR", + "clause": "Art. 28(3), Art. 28(10)", + "cross_reference": "DPDP s.8(2), Rule 6(1)(f)", + "issue": "The Quantlytics order form is not a processor contract. It has no documented instructions, confidentiality, security, sub-processor, rights assistance, breach notice, deletion or audit terms. Its terms are an online document 'as updated from time to time', which was not reviewed. Line 15 lets Quantlytics use event and profile data to operate and improve its products, which is use for its own purposes and points to controller status under Art. 28(10), while the notice says analytics providers process data 'for us'. The customer is Kadamba Fit Private Limited alone, signed by the Head of Product, so the Irish controller has no contract. GDPR applies today. DPDP readiness gap: s.8(2) and Rule 6(1)(f) apply from 13 May 2027.", + "recommendation": "Sign a DPA with Art. 28(3) terms and a Rule 6(1)(f) security clause, with both Kadamba entities as parties. Remove the own-use grant, or treat Quantlytics as a controller and disclose that. Route vendor contracts through the DPO.", + "file": "docs/vendors/quantlytics-order-form.md", + "line": 15, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0013", + "severity": "high", + "framework": "GDPR", + "clause": "Art. 5(1)(b), Art. 6(1), Art. 13(1)(c), Art. 21(2)", + "cross_reference": "DPDP s.4, s.5(1), s.6(1)", + "issue": "Sponsored offers profile members with no basis or disclosure. GET /me/offers sorts members into buyer segments from 90 days of workouts and ranks offers from ad partners (lines 6-16). Workouts were collected to show history (docs/ropa.md:8). No consent purpose covers this (config/app.yaml:7-11), marketing consent is not checked, the notice, RoPA and vendor register do not mention it or the ad partners, and Settings has no opt-out. The offer_feed code is not in the repository, so whether the segment leaves Kadamba could not be confirmed. GDPR applies today. DPDP readiness gap: these clauses apply from 13 May 2027.", + "recommendation": "Switch the feature off until it has a separate opt-in purpose. Then add it to the notice and RoPA, list the ad partners with contracts, and give members an off switch in Settings.", + "file": "services/offers.py", + "line": 9, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0014", + "severity": "high", + "framework": "DPDP", + "clause": "s.4, s.6(1), s.9(3)", + "cross_reference": "GDPR Art. 5(1)(a)", + "issue": "DPDP readiness gap: these clauses apply from 13 May 2027. Analytics for India members has no DPDP ground, and it tracks children. The RoPA (line 14) and notice rely on legitimate interests, which DPDP does not have, and s.7 does not list product improvement. config/app.yaml:7-11 has no analytics purpose and track() checks no consent, age or guardian, so a child's events and IP address go to Quantlytics, which is tracking under s.9(3). No caller of track() is in the repository, so which events are sent could not be confirmed.", + "recommendation": "Add an opt-in analytics purpose for India members and gate track() on it. Block tracking for anyone under 18 in both regions, using dob or guardian_id.", + "file": "services/analytics.py", + "line": 6, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0015", + "severity": "high", + "framework": "GDPR", + "clause": "Art. 32(1)(a)", + "cross_reference": "DPDP s.8(5), Rule 6(1)(a)", + "issue": "Going by the manifests, coach tax IDs and bank accounts are stored in plaintext. seal() returns the raw bytes when FIELDCRYPT_KEY is unset (line 11). deploy/deployment.yaml sets only APP_ENV, CONFIG_PATH and CRM_API_KEY and loads the kadamba-api secret, deploy/secrets.yaml holds only DATABASE_URL, and config/env.example leaves the key blank. The columns are named _sealed and carry a key_alias default (db/migrations/0006_coach_payouts.sql:7) that no code reads, so the data looks encrypted when it is not. A key injected outside these manifests cannot be ruled out from the repository. GDPR applies today. DPDP readiness gap: Rule 6(1)(a) applies from 13 May 2027.", + "recommendation": "Fail closed: raise at startup if the key is missing and delete the plaintext branch. Provide the key through the secret manager or use the KMS alias the schema implies. Check which rows are plaintext and re-encrypt them.", + "file": "utils/fieldcrypt.py", + "line": 11, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0016", + "severity": "high", + "framework": "GDPR", + "clause": "Art. 25(1), Art. 32(1)(a)", + "cross_reference": "DPDP Rule 6(1)(a)", + "issue": "The masking script would not make staging safe even if it ran. It overwrites only full_name, email, phone and nominee on members. dob and push_token stay. health_samples, weekly_summaries, cycle_logs with their free-text notes, route_tracks, tickets with free text, and invoices with billing name, billing email and tax number are untouched, and all remain linkable through member_ref. GDPR applies today. DPDP readiness gap: Rule 6(1)(a) applies from 13 May 2027.", + "recommendation": "Replace it with synthetic seed data. If masking stays, cover every personal column in every table, re-key member_ref, drop free-text fields, and test that no production value survives.", + "file": "scripts/mask_staging.sql", + "line": 1, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0017", + "severity": "high", + "framework": "GDPR", + "clause": "Art. 15(1), Art. 15(1)(c), Art. 15(3), Art. 20(1)", + "cross_reference": "DPDP s.11(1)(a), s.11(1)(b)", + "issue": "GET /me/export returns only profile, workouts and bookings. It leaves out route_tracks, health_samples, weekly_summaries, cycle_logs, invoices, tickets, consent_events and dependants' records, though the notice (web/privacy-notice.html:31) promises a copy of your data. It gives no list of recipients or what each received, so a member cannot see what went to Harbourline. Wearable and cycle data the member supplied are also missing from any Art. 20 copy. GDPR applies today. DPDP readiness gap: s.11 applies from 13 May 2027.", + "recommendation": "Extend EXPORT_SECTIONS to every table holding the member's data, add the recipients and what each received, add dependants for the parent, and test the export against the schema.", + "file": "services/rights.py", + "line": 4, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0018", + "severity": "high", + "framework": "GDPR", + "clause": "Art. 19", + "cross_reference": "DPDP s.8(7), s.12(3)", + "issue": "Erasure reaches Quantlytics and Cadencehive (lines 12-13) and stops there. Nothing tells Harbourline, which holds the weekly data and, per F-0001, email, date of birth and health averages. Withdrawing wellness_partner (api/consents.py:10) stops the next export and tells Harbourline nothing. The Mailwren DPA deletes at the end of the service (docs/vendors/mailwren-dpa.md:22) and no per-member path is shown. GDPR applies today. DPDP readiness gap: these clauses apply from 13 May 2027.", + "recommendation": "Add a deletion and withdrawal call to Harbourline, log it, and write the duty into the partner agreement. Add a per-member path for Mailwren or record how message content ages out.", + "file": "jobs/purge_closed.py", + "line": 12, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0019", + "severity": "high", + "framework": "DPDP", + "clause": "s.8(7), s.9(1)", + "cross_reference": "GDPR Art. 5(1)(e), Art. 17(1)", + "issue": "DPDP readiness gap: these clauses apply from 13 May 2027; the GDPR clauses apply today. Family plans break closure and purge. close_account changes only the parent (services/account_lifecycle.py:6), so dependants stay active with no working guardian. erase_member then deletes the parent row, which db/migrations/0001_members.sql:9 references through guardian_id with no cascade, so the delete fails for any parent with a dependant. The loop on lines 20-21 has no error handling, so that failure ends the nightly run: later members and the invoice, consent and ticket deletions on lines 22-30 do not run. DELETE /me hits the same failure (services/rights.py:24).", + "recommendation": "Decide what happens to dependants when a guardian leaves, and implement it: erase, or move to another verified guardian. Delete dependants first, handle errors per member so one failure cannot stop the job, and alert on failures.", + "file": "jobs/purge_closed.py", + "line": 14, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0020", + "severity": "high", + "framework": "GDPR", + "clause": "Art. 33(1)", + "cross_reference": "DPDP Rule 7(2)(b)", + "issue": "The 72-hour clock starts at the wrong moment. The matrix counts from the Sev 1 declaration, while Art. 33(1) and Rule 7(2)(b) count from becoming aware, so triage time falls outside the deadline. GDPR applies today. DPDP readiness gap: Rule 7 applies from 13 May 2027.", + "recommendation": "Count 72 hours from the moment of awareness, record that time at intake, and name who decides. Keep the DPO in the first hour of the incident.", + "file": "docs/runbooks/severity-matrix.md", + "line": 5, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0021", + "severity": "high", + "framework": "DPDP", + "clause": "Rule 7(2)(a)", + "cross_reference": "GDPR Art. 33(4)", + "issue": "DPDP readiness gap: Rule 7 applies from 13 May 2027. The runbook has no initial intimation to the Data Protection Board without delay. Only one report within 72 hours appears, and only for Sev 1.", + "recommendation": "Add a step that sends the Board an initial description (nature, extent, timing, location, likely impact) without delay, followed by the detailed report within 72 hours. Do the same in phases for the EU authority.", + "file": "docs/runbooks/severity-matrix.md", + "line": 5, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0022", + "severity": "high", + "framework": "DPDP", + "clause": "Rule 7(1), s.8(6)", + "cross_reference": "GDPR Art. 34(1)", + "issue": "DPDP readiness gap: Rule 7 applies from 13 May 2027; GDPR Art. 34 applies today. Telling members is optional and late. For Sev 1 the Head of Communications decides on member messaging after the review, and Sev 2 and 3 have no member step at all. Rule 7(1) requires each affected Data Principal to be told without delay, with no harm threshold. Art. 34(1) requires it without undue delay where the risk is high, which a leak of cycle or children's data would be.", + "recommendation": "Make member intimation a required runbook step with an owner and a template holding the Rule 7(1) contents. For the EU, have the DPO make and record the Art. 34 risk decision before any review.", + "file": "docs/runbooks/severity-matrix.md", + "line": 5, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0023", + "severity": "high", + "framework": "DPDP", + "clause": "s.8(6)", + "cross_reference": "GDPR Art. 33(1), Art. 33(5)", + "issue": "DPDP readiness gap: s.8(6) applies from 13 May 2027; GDPR Art. 33 applies today. Severity by record count removes notification for most incidents. Sev 2 (100 to 10,000 records) reaches the DPO at a weekly review, which can be after 72 hours, and Sev 3 is a ticket with an optional postmortem. Neither notifies the Board or the EU authority. DPDP has no threshold, and Art. 33(1) turns on risk and not on volume, so 50 cycle logs or children's records would be Sev 3 with no notice. The only data-type trigger is payment tokens. Art. 33(5) also requires every breach to be documented.", + "recommendation": "Separate operational severity from legal notification. Send every personal data incident to the DPO at once, treat health, children's and identity data as high risk whatever the count, and log every breach with its facts, effects and remedial action.", + "file": "docs/runbooks/severity-matrix.md", + "line": 6, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0024", + "severity": "high", + "framework": "DPDP", + "clause": "Rule 6(1)(e), Rule 8(3)", + "cross_reference": "CERT-In Directions 2022; GDPR Art. 5(2)", + "issue": "Logs are kept for 30 days. All three Loki tenants have retention 720h (lines 9, 11, 13). CERT-In requires 180 days of ICT logs in India, and that applies today. The retention schedule promises 12 months for access and auth logs (docs/retention-schedule.md:12), so the schedule and the system disagree. Rule 6(1)(e) and Rule 8(3) require one year: a DPDP readiness gap until 13 May 2027. An incident found after 30 days has no logs to investigate.", + "recommendation": "Fix F-0005 first so bodies and tokens are not retained longer. Then set api-access and auth-events to at least one year, keep ICT logs in India for at least 180 days, and set a separate short period for application logs in the schedule.", + "file": "deploy/logging.yaml", + "line": 9, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0025", + "severity": "medium", + "framework": "GDPR", + "clause": "Art. 5(1)(c), Art. 6(1)(f), Art. 21(1), Art. 21(4)", + "issue": "Analytics sends more than it needs and its legitimate-interests claim is unsupported. distinct_id already identifies the profile, yet each event adds email and IP address (lines 8-10). A legitimate interests assessment was not found in the reviewed material. Settings has no switch to object and the notice does not mention the right to object.", + "recommendation": "Drop email and truncate or drop IP. Write the balancing test, add an objection switch that gates track(), and state the right in the notice.", + "file": "services/analytics.py", + "line": 9, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0026", + "severity": "medium", + "framework": "DPDP", + "clause": "s.8(6), Rule 7", + "cross_reference": "GDPR Art. 33(1), Art. 33(5)", + "issue": "DPDP readiness gap: these clauses apply from 13 May 2027; GDPR Art. 33 applies today. The runbook has no step that asks whether personal data is involved, and none that records when the organisation became aware. Step 2 records an unspecified time when severity is assigned. Without an awareness time, the 72-hour deadline cannot be shown to have been met.", + "recommendation": "Add an intake step that records aware_at, whether personal data is involved, the categories and the approximate number of people, and sends it to the DPO.", + "file": "docs/runbooks/incident-response.md", + "line": 2, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0027", + "severity": "medium", + "framework": "DPDP", + "clause": "Rule 14(3), s.13(2)", + "cross_reference": "GDPR Art. 12(2)", + "issue": "DPDP readiness gap: these clauses apply from 13 May 2027. grievance_resolution is 2880 hours, which is 120 days and over the 90-day ceiling, and services/grievance.py:15 sets due_at from it. The notice publishes no response period (web/privacy-notice.html:33), which Rule 14(3) requires. rights_request at 600 hours (25 days) is inside the one-month GDPR limit, so that one is fine, but an EU complaint that is really a rights request filed as a grievance gets the longer clock.", + "recommendation": "Set grievance_resolution to 2160 hours or fewer, publish the period in the notice, and have the privacy desk reclassify rights requests filed as grievances.", + "file": "config/support.yaml", + "line": 4, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0028", + "severity": "medium", + "framework": "GDPR", + "clause": "Art. 13(2)(b), Art. 18, Art. 21(4)", + "issue": "The notice's rights list omits restriction, portability and objection. Art. 21(4) requires the right to object to be presented clearly and separately, and analytics rests on legitimate interests. No route for restriction or objection was found in Settings or services/rights.py either.", + "recommendation": "List all Art. 15-22 rights in the notice, state the right to object separately, and add restriction and objection routes through Settings or the privacy desk.", + "file": "web/privacy-notice.html", + "line": 29, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0029", + "severity": "medium", + "framework": "GDPR", + "clause": "Art. 13(1)(c), Art. 13(1)(e), Art. 13(2)(a), Art. 13(2)(e)", + "cross_reference": "DPDP Rule 3(b)", + "issue": "The notice table leaves out data the code holds: billing name and email, tax number and payment token (db/migrations/0005_billing.sql), support tickets (0007_support.sql), device serial and children's records. It names no SMS provider, push provider, payment provider or ad partner. It gives no retention for tickets, logs or snapshots, and does not say which data is needed for the contract. A notice for coaches, whose tax IDs and bank details are held, was not found in the reviewed material. GDPR applies today. DPDP readiness gap: Rule 3 applies from 13 May 2027.", + "recommendation": "Add every category and recipient to the notice with its purpose, basis and retention period, say which data is mandatory, and issue a separate notice to coaches.", + "file": "web/privacy-notice.html", + "line": 10, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0030", + "severity": "medium", + "framework": "GDPR", + "clause": "Art. 12(1)", + "cross_reference": "DPDP s.5(3)", + "issue": "The notice is offered only in English and Hindi. The app signs up members in Germany (web/signup.html:4) with no German version. For India, s.5(3) gives the option of any Eighth Schedule language, and no route to the others is shown: a DPDP readiness gap until 13 May 2027.", + "recommendation": "Publish a German notice and consent text. Add the Eighth Schedule language option, on request at least, before May 2027.", + "file": "web/privacy-notice.html", + "line": 5, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0031", + "severity": "medium", + "framework": "GDPR", + "clause": "Art. 30(1)", + "issue": "The RoPA lacks required fields and activities. It has no categories of data subjects (Art. 30(1)(c)), third-country transfers and safeguards (e), retention (f) or security measures (g). Missing activities are sponsored offers, family plan and children's data, support tickets, the staging refresh and SMS OTP. The wellness row (line 17) lists member ref and tier while the job sends more. The Bengaluru engineering team, the ad partners and the SMS provider are not listed as recipients.", + "recommendation": "Add the missing columns and activities, and check the RoPA against the code and vendor register at each review.", + "file": "docs/ropa.md", + "line": 5, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0032", + "severity": "medium", + "framework": "GDPR", + "clause": "Art. 28(1), Art. 30(1)(d)", + "cross_reference": "DPDP s.8(2)", + "issue": "Several recipients are missing from the vendor register: the SMS provider (services/otp.py:13), the push provider (jobs/reminder_push.py:9), the ad partners behind offer_feed (services/offers.py:9), and the payment provider and bank (docs/ropa.md:12-13). Their contracts and locations were not found in the reviewed material. The Cadencehive DPA is listed as held by Legal and was not reviewed, and config/production.yaml has no regional CRM endpoint, so the 'EU region only' entry could not be verified. DPDP readiness gap: s.8(2) applies from 13 May 2027.", + "recommendation": "Complete the register with role, location, contract and transfer tool for each recipient, and collect the missing contracts. Confirm that Cadencehive EU contacts stay in eu-west-1.", + "file": "docs/vendors/register.md", + "line": 3, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0033", + "severity": "medium", + "framework": "GDPR", + "clause": "Art. 5(2), Art. 7(1)", + "cross_reference": "DPDP s.6(10)", + "issue": "Proof of consent can be deleted too early. The schedule keeps consent events and tickets for 3 years after closure (docs/retention-schedule.md:10-11), but the job deletes on created_at and opened_at older than 3 years once the member row is gone (lines 24, 28). A member who consented 4 years ago and closes today loses that proof about 30 days later. GDPR applies today. DPDP readiness gap: s.6(10) applies from 13 May 2027.", + "recommendation": "Keep the closure date after the member row is erased, for example in a small table keyed by member_ref, and delete 3 years after that date.", + "file": "jobs/purge_closed.py", + "line": 24, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0034", + "severity": "medium", + "framework": "GDPR", + "clause": "Art. 5(1)(e)", + "cross_reference": "DPDP s.8(7), Rule 8", + "issue": "The retention schedule has gaps. coach_payouts (tax ID and bank account) has no entry and no delete path (services/coach_payouts.py). Every member period starts at closure, so an abandoned account keeps raw heart-rate, SpO2, sleep samples and GPS routes with no limit, and nothing removes raw samples once weekly summaries exist. Retention for data held by Quantlytics and Cadencehive was not found in the reviewed material. The Third Schedule three-year limit lists large e-commerce, gaming and social media platforms, so it sets no period here, and the s.8(7) purpose test still applies. GDPR applies today. DPDP readiness gap: these clauses apply from 13 May 2027.", + "recommendation": "Add coach payout retention with a legal basis. Set an inactivity limit and a shorter life for raw samples and routes, and record vendor retention.", + "file": "docs/retention-schedule.md", + "line": 5, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0035", + "severity": "medium", + "framework": "GDPR", + "clause": "Art. 35(1), Art. 35(3)(b)", + "issue": "A DPIA is required, one is reported and it was not reviewed. The README says a DPIA for wearable and cycle processing was completed on 12 March 2026 and is held by the DPO outside the repository. Not found in the reviewed material: any DPIA covering insurer sharing, family-plan processing of children's data, sponsored-offer profiling or US analytics. The partner agreement was signed on 1 September 2025 and other vendor contracts earlier, so check that assessment came before processing. DPDP s.10(2)(c) does not apply because the company is not designated an SDF. A draft for the uncovered processing is at company-profile/kadamba-cold/dpias/dpia-2026-11-partner-children-offers.json.", + "recommendation": "Have the DPO confirm the scope of the March 2026 DPIA, review it against the current code, and complete the draft for the processing it does not cover.", + "file": "README.md", + "line": 19, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0036", + "severity": "medium", + "framework": "GDPR", + "clause": "Art. 16, Art. 19", + "cross_reference": "DPDP s.12", + "issue": "Corrections stay in the members table. PATCH /me updates the row only, and Quantlytics and Cadencehive are not told. campaign_sync upserts by email, so a changed email leaves the old contact in the CRM, where crm.delete_contact(member.email) will not find it at erasure. dob is editable with no age re-check and is_minor is not recomputed. GDPR applies today. DPDP readiness gap: s.12 applies from 13 May 2027.", + "recommendation": "Send corrections to recipients, key CRM contacts on member_ref, and re-run the age rules when dob changes.", + "file": "services/rights.py", + "line": 19, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0037", + "severity": "low", + "framework": "GDPR", + "clause": "Art. 6(1)(c), Art. 6(3)", + "issue": "The nine-year invoice period cites only Indian statutes. Keeping invoices after erasure is correct in principle and is not the issue. For Kadamba Fit Ireland Limited, Art. 6(3) needs EU or Member State law, the schedule cites none, and the notice gives one nine-year period to everyone. The right Irish period was not checked in this audit.", + "recommendation": "Have counsel confirm the Irish and German invoice retention periods and add a separate row for the EU entity.", + "file": "docs/retention-schedule.md", + "line": 9, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0038", + "severity": "low", + "framework": "GDPR", + "clause": "Art. 7(3), Art. 12(2)", + "cross_reference": "DPDP s.6(4), s.12", + "issue": "Settings shows no consent state and no erasure link. The boxes render unticked and the script handles only changes, so a member who consented sees an unticked box and must tick then untick to withdraw, which writes a false grant event. fetch failures are not handled. The notice says erasure is available in Settings (web/privacy-notice.html:31), but the page offers only Close my account, with its 30-day wait, and has no link to DELETE /me. The file has no template logic, so state may be filled in elsewhere; that was not verified. GDPR applies today. DPDP readiness gap: these clauses apply from 13 May 2027.", + "recommendation": "Load current state from consent_ledger, show errors when a change fails, and add an erasure action or correct the notice.", + "file": "web/settings.html", + "line": 3, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0039", + "severity": "low", + "framework": "GDPR", + "clause": "Art. 5(1)(c), Art. 13(1)(c)", + "cross_reference": "DPDP s.6(1)", + "issue": "predict_next uses only start dates, yet log_cycle also stores symptoms and free-text notes. Notes are not in the notice (web/privacy-notice.html:15). health_samples stores device_serial with no purpose stated in the RoPA or notice. GDPR applies today. DPDP readiness gap: s.6(1) applies from 13 May 2027.", + "recommendation": "Say what symptoms and notes are for and disclose them, or stop collecting them. Drop device_serial unless a purpose needs it.", + "file": "services/cycle.py", + "line": 6, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + }, + { + "id": "F-0040", + "severity": "low", + "framework": "GDPR", + "clause": "Art. 13(2)(f), Art. 22", + "issue": "The weekly tier feeds a premium discount. Whether this is a solely automated decision with a similarly significant effect under Art. 22 depends on how Harbourline sets the discount, which the agreement does not say. The notice gives no logic or consequence for the tier, and no code in the repository writes weekly_summaries. This is an open legal question and was not resolved here.", + "recommendation": "Ask counsel whether Art. 22 applies, get the discount logic from Harbourline, and describe the tier and its effect in the notice.", + "file": "docs/vendors/harbourline-partner-agreement.md", + "line": 5, + "status": "open", + "found_at": "2026-11-05T17:24:36Z" + } + ] +} diff --git a/examples/kadamba-fit.run-1/score-check.mjs b/examples/kadamba-fit.run-1/score-check.mjs new file mode 100644 index 0000000..de7bce9 --- /dev/null +++ b/examples/kadamba-fit.run-1/score-check.mjs @@ -0,0 +1,72 @@ +// Scoring checks for the kadamba-fit cold audit. Run from the repo root: +// node examples/kadamba-fit.run-1/score-check.mjs examples/kadamba-fit examples/kadamba-fit.expected.json +import { readFileSync, readdirSync, statSync, existsSync } from 'node:fs'; +import { join } from 'node:path'; + +const [appDir, keyFile] = process.argv.slice(2); +const dir = 'examples/kadamba-fit.run-1'; +const r = JSON.parse(readFileSync(join(dir, 'report.json'), 'utf8')); +const key = JSON.parse(readFileSync(keyFile, 'utf8')); +const by = Object.fromEntries(r.findings.map(f => [f.id, f])); +// Which findings cover which planted issue: the scorer's judgement, recorded in score.md +const map = { P1: ['F-0002'], P2: ['F-0007', 'F-0012'], P3: ['F-0011'], P4: ['F-0010'], P5: ['F-0008'], P6: ['F-0020', 'F-0021', 'F-0022', 'F-0023', 'F-0026'], P7: ['F-0005'], P8: ['F-0017'], P9: ['F-0015'], P10: ['F-0006'], P11: ['F-0004', 'F-0016'], P12: ['F-0024'], P13: ['F-0003'], P14: ['F-0001'], P15: ['F-0027'], P16: ['F-0009'] }; + +const norm = t => t.replace(/Section\s+/g, 's.').replace(/\s+/g, ' '); +const tokens = c => c.split(/[;,]/).map(x => x.replace(/^\s*(DPDP|GDPR)\s+/, '').trim()).filter(Boolean).map(x => x.startsWith('CERT-In') ? 'CERT-In' : x); +const top = t => (t.match(/^(s\.\d+|Rule \d+|Art\. \d+)/) || [t])[0]; +// Token present and not followed by a digit, so s.1 does not match s.11 +const has = (text, tok) => { for (let i = text.indexOf(tok); i !== -1; i = text.indexOf(tok, i + 1)) if (!/[0-9]/.test(text[i + tok.length] ?? '')) return true; return false; }; + +let exact = 0, level = 0, total = 0, sheetRight = 0; +console.log('--- citation anchors (exact-or-deeper / top-level)'); +for (const e of key.expected) { + const text = norm(map[e.id].map(id => `${by[id].clause}; ${by[id].cross_reference ?? ''}`).join(' ; ')); + const res = tokens(e.clause).map(t => { total++; const ex = has(text, t), lv = ex || has(text, top(t)); if (ex) exact++; if (lv) level++; return `${t}=${ex ? 'exact' : lv ? 'level' : 'MISS'}`; }); + if (res.some(x => !x.endsWith('MISS'))) sheetRight++; + const files = map[e.id].map(id => by[id].file); + console.log(e.id, map[e.id].join(','), '|', res.join(' '), '| file', files.includes(e.file) ? 'match' : 'OTHER:' + files.join(',')); +} +console.log(`exact ${exact}/${total}, top-level ${level}/${total}, run-sheet citation right ${sheetRight}/${key.expected.length}`); +const mapped = new Set(Object.values(map).flat()); +console.log('mapped', mapped.size, 'other', r.findings.filter(f => !mapped.has(f.id)).map(f => f.id).join(',')); + +console.log('--- should-not-flag: findings filed on the file / naming it'); +for (const s of key.should_not_flag) console.log(s.file, '| filed:', r.findings.filter(f => f.file === s.file).map(f => f.id).join(',') || '-', '| named in issue:', r.findings.filter(f => f.file !== s.file && f.issue.includes(s.file)).map(f => f.id).join(',') || '-'); +console.log('Singapore/ap-southeast-1/backup mentions:', r.findings.filter(f => /Singapore|ap-southeast|backup\.yaml/i.test(f.issue + f.recommendation)).map(f => f.id).join(',') || 'none'); + +console.log('--- paths'); +const isAbs = p => /^[A-Za-z]:/.test(p) || p.startsWith('/') || p.startsWith(String.fromCharCode(92)) || p.startsWith('../'); +const bad = r.findings.filter(f => f.file && (isAbs(f.file) || !existsSync(join(appDir, f.file)) || readFileSync(join(appDir, f.file), 'utf8').split('\n').length < (f.line || 1))); +console.log('findings with file', r.findings.filter(f => f.file).length, '| absolute, missing or line past end:', bad.map(f => `${f.id} ${f.file}:${f.line}`).join(', ') || 'none'); + +console.log('--- commencement: DPDP cited but no 13 May 2027 timing in issue'); +console.log(r.findings.filter(f => (f.framework === 'DPDP' || /DPDP|s\.\d|Rule \d/.test(f.cross_reference ?? '')) && !f.issue.includes('13 May 2027')).map(f => f.id).join(',') || 'none'); + +console.log('--- values (names only, never the values)'); +// Every value is read from the app at run time, so this file holds none of them +const encoded = readFileSync(join(appDir, 'deploy/secrets.yaml'), 'utf8').match(/DATABASE_URL: (\S+)/)[1]; +const decoded = Buffer.from(encoded, 'base64').toString(); +const values = { + secretEncoded: encoded, + secretDecoded: decoded, + secretUserinfo: decoded.split('@')[0].split('//')[1], + secretHost: decoded.split('@')[1].split(':')[0], + envExampleUrl: readFileSync(join(appDir, 'config/env.example'), 'utf8').match(/DATABASE_URL=(\S+)/)[1], +}; +JSON.parse(readFileSync(join(appDir, 'tests/fixtures/members.json'), 'utf8')).forEach((m, i) => Object.assign(values, { [`fixture${i}.email`]: m.email, [`fixture${i}.phone`]: m.phone, [`fixture${i}.name`]: m.full_name })); +const walk = d => readdirSync(d).flatMap(n => { const p = join(d, n); return statSync(p).isDirectory() ? walk(p) : [p]; }); +const files = walk(dir).filter(f => !f.endsWith('score-check.mjs') && !f.endsWith('score.md')); +let leaks = 0; +for (const f of files) { const t = readFileSync(f, 'utf8'); const hits = Object.entries(values).filter(([, v]) => t.includes(v)).map(([k]) => k); if (hits.length) { leaks++; console.log('HIT', f, hits.join(',')); } } +console.log('files checked', files.length, '| files with a value', leaks); + +console.log('--- clauses outside the catalogs'); +const all = r.findings.map(f => `${f.clause} ${f.cross_reference ?? ''}`).join(' '); +const onMap = n => [[1, 10], [12, 22], [24, 26], [28, 28], [30, 30], [32, 39], [44, 49], [77, 84]].some(([a, b]) => n >= a && n <= b); +const arts = [...new Set([...all.matchAll(/Art\.\s*(\d+)/g)].map(m => +m[1]))].sort((a, b) => a - b); +console.log('articles', arts.join(','), '| off map:', arts.filter(a => !onMap(a)).join(',') || 'none'); +console.log('other instruments:', [...new Set([...all.matchAll(/(CERT-In[^;,]*|CCPA[^;,]*|RBI[^;,]*|IT Act[^;,]*)/g)].map(m => m[1].trim()))].join(' | ') || 'none'); +const sar = JSON.parse(readFileSync(join(dir, 'findings.sarif'), 'utf8')); +const uris = sar.runs[0].results.map(x => x.locations[0].physicalLocation.artifactLocation.uri); +console.log('SARIF results', uris.length, '| absolute uris', uris.filter(isAbs).length); +console.log('severity', JSON.stringify(r.findings.reduce((a, f) => (a[f.severity] = (a[f.severity] || 0) + 1, a), {}))); diff --git a/examples/kadamba-fit.run-1/score.md b/examples/kadamba-fit.run-1/score.md new file mode 100644 index 0000000..d73b52a --- /dev/null +++ b/examples/kadamba-fit.run-1/score.md @@ -0,0 +1,129 @@ +# Score: cold audit of kadamba-fit + +Assessment `2026-11-05-kadamba-cold-full`, 40 findings, run on master at `259d7e0` against the app from `b6d1c44`. Scored on 8 November 2026 against `examples/kadamba-fit.expected.json` and the run sheet on branch `claude/trusting-wilbur-d90e22`. The key and app are identical at `b6d1c44` and `3c9e897`. After this score, `a2083c0` corrected the key and run sheet; the app is unchanged. These figures use the earlier key. + +The scorer did not write the report, and opened the key only for scoring. `report.json` has the same sha256 before and after scoring: `751bc5a92f9fcb550cf9e8701f0a295d6f1774d6e85a97b96a70efb1cebabb34`. + +This folder holds the run's `report.json` and `summary.md`, copied from the gitignored assessment folder. The exports and company profile were not copied. `score-check.mjs` reproduces the mechanical checks: + +```bash +node examples/kadamba-fit.run-1/score-check.mjs examples/kadamba-fit examples/kadamba-fit.expected.json +``` + +Run against the corrected key, it prints the same planted-issue and value checks; the should-not-flag notes have changed. + +## Result + +| Measure | Score | +|---|---| +| Planted issues found | 16 of 16 | +| Partly found or missed | 0 | +| Citation right, run-sheet rule | 16 of 16 | +| Citation anchors, exact or more specific | 45 of 48 | +| Citation anchors, at section, rule or article level | 47 of 48 | +| Should-not-flag violations, by the key | 3 of 13 | +| Other findings: real unplanted gaps / disputed / false positive | 14 / 3 / 1 | +| DPDP findings that read as current violations | 0 | +| Files containing a fixture or config value | 0 of 11 | +| Findings with an absolute or missing path | 0 of 40 | +| Clauses outside the catalogs | None | + +## Blindness + +The transcript never mentions the kadamba-fit key or run sheet. The only hits for `expected.json` are git status lines naming the acme-demo key. + +The run did not follow the run sheet's setup. It audited a `git archive` of the app folder from inside the main checkout, not from an exported copy of Vigil, and it used company id `kadamba-cold`. The key sat on disk in `.claude/worktrees/trusting-wilbur-d90e22/` throughout and was never opened. Before starting, the run confirmed that its working tree matched PR #2's head. + +## Planted issues + +| ID | Issue | Findings | Verdict | Clause check | +|---|---|---|---|---| +| P1 | Purge skips the three `member_ref` tables | F-0002 | Found | All 4 exact | +| P2 | EU analytics to a US endpoint, no mechanism or DPA | F-0007, F-0012 | Found | All 3 exact | +| P3 | Campaign sync ignores withdrawal | F-0011 | Found | Both exact | +| P4 | Insurer sharing required at signup | F-0010 | Found | Both exact | +| P5 | Dependants never marked minor, so they get ad offers | F-0008 | Found | s.9(3) exact; GDPR Art. 8(1) not cited | +| P6 | Severity thresholds and the 72-hour clock | F-0020, F-0021, F-0022, F-0023, F-0026 | Found | All 5 exact | +| P7 | Middleware logs bodies at DEBUG in production | F-0005 | Found | All 3 exact | +| P8 | Export omits tables and recipients | F-0017 | Found, though it doesn't name `docs/vendors/register.md` | Both exact | +| P9 | `seal()` stores plaintext without its key | F-0015 | Found | All 3 exact | +| P10 | Base64 database URL in `deploy/secrets.yaml` | F-0006 | Found, though it doesn't name `config/production.yaml` | All 3 exact | +| P11 | Unmasked production data in staging | F-0004, F-0016 | Found | All 5 exact | +| P12 | Logs kept 30 days | F-0024 | Found | Both exact | +| P13 | No Art. 9(2) condition for health data | F-0003 | Found | Both exact | +| P14 | Partner export sends the whole row | F-0001 | Found | 3 exact; Art. 5(1)(b) only at article level | +| P15 | Grievance clock of 120 days | F-0027 | Found | Both exact | +| P16 | Dependant added with no parental consent | F-0009 | Found | 3 exact; Art. 8(1) only at article level, through Art. 8(2) | + +Every found item points at the file the key names. + +## Should-not-flag + +These are scored by aspect, because `config/support.yaml` is both P15 and a should-not-flag item. + +| File | Verdict | +|---|---| +| `services/otp.py` | Clean. F-0005 says the middleware defeats its masking, which is true | +| `db/migrations/0005_billing.sql` | Clean | +| `docs/vendors/mailwren-dpa.md` | Clean on the key's points. F-0018 says Mailwren has no per-member deletion path, which is weak because DPA clause 5 covers help with requests | +| `docs/vendors/register.md` | Clean. F-0032 flags missing vendors, not the Bengaluru SCCs | +| `docs/retention-schedule.md` | **Violated** by F-0037, which faults the nine-year invoice row for the Irish entity | +| `db/migrations/0002_consent_events.sql` | Clean. F-0033 reports the opposite problem, deletion too early | +| `deploy/backup.yaml` | Clean, never mentioned | +| `config/env.example` | Clean. F-0006 says it needs no change | +| `jobs/reminder_push.py` | Clean. F-0011 credits it | +| `web/settings.html` | **Violated** by F-0038, which cites Art. 7(3) and s.6(4) on the switches | +| `config/support.yaml` (rights clock) | Clean. F-0027 says 600 hours is fine | +| `tests/fixtures/members.json` | Clean, no finding | +| `web/privacy-notice.html` | **Violated** by F-0028, which reports rights missing from the notice. F-0025 repeats the objection point | + +Two of the three violations look like key defects: + +- **F-0028:** the notice omits restriction and objection, which Art. 13(2)(b) requires. The key's blanket "rights" protection is too broad. +- **F-0037:** the schedule justifies nine years with Indian statutes only, and Art. 17(3)(b) and Art. 6(3) need EU or Member State law for the Irish controller. + +The third is a real false positive. F-0038's withdrawal claim assumes the switches render without the member's current state, but the file is a static fixture and the finding admits that state may be filled elsewhere. Its separate erasure-link point is a real gap: the notice promises erasure in Settings and the page doesn't offer it. + +## Other findings + +18 findings fall outside the planted issues. The 3 disputed ones are F-0028, F-0037 and F-0038, covered above. + +| Verdict | Findings | +|---|---| +| Real unplanted gap, checked against the code | F-0013, F-0014, F-0018, F-0019, F-0025, F-0029, F-0030, F-0031, F-0032, F-0033, F-0034, F-0035, F-0036, F-0039 | +| False positive | F-0040: an open Art. 22 question with no evidence of a solely automated decision | + +Three of the real gaps are code defects the key's author missed: + +- **F-0019:** `guardian_id` has no cascade, so erasing a parent fails, and the purge loop has no error handling, so one failure ends the nightly run. +- **F-0033:** consent proof is deleted by `created_at`, so it can go about 30 days after closure instead of three years later. The key's should-not-flag note claims the purge waits for the end of the period, which is wrong for older consents. +- **F-0036:** a corrected email leaves the old contact in the CRM, where the erasure call cannot find it. + +## The audit session's guess points + +| # | Point | Verdict | +|---|---|---| +| 1 | One `legal_name` and address for two entities | Real schema gap | +| 2 | `dpo` holds only a mailbox | Real schema gap, minor | +| 3 | Whether README claims count as posture evidence | Judgement call | +| 4 | UK and IRDAI left unknown | Judgement call | +| 5 | Meaning of "realistic-format" in `/pii-scan` | Wording gap. The outcome was right: zero-filled phones are valid format but plainly synthetic | +| 6 | Production credential marked synthetic, rated by risk | Judgement call, and the right one | +| 7 | A DPIA reported but not supplied | Real skill gap: the absence rule doesn't cover it | +| 8 | Sector check ignores an insurer partner | Real skill gap, low impact | +| 9 | Breach readiness has no rule for a step that is present but wrong | Real skill gap. It drove P6, and the run handled it anyway | +| 10 | "Always high" for dark patterns | Wording gap: it should say "at least high" | +| 11 | One `lawful_basis` value; `transfer_mechanism` has no DPDP value | Real schema gap for the second; the first is handled by `none_identified` | +| 12 | One `file` and `line` per finding | Real format gap. SARIF could carry related locations | +| 13 | Own coverage count; one posture dimension per finding | Judgement call, by design | +| 14 | Sub-skills applied inline | Process note, not a gap | + +## Comparison with acme-demo + +Acme-demo, self-scored on a fixture whose comments named its bugs, found 9 of 10 with 23 of 27 exact anchors. Kadamba-fit, scored by a separate session on a fixture with no telltale comments, found 16 of 16 with 45 of 48 exact anchors. It also violated 3 of 13 should-not-flag items, two of them arguably defects in the key. + +## Limits + +- A Claude session wrote the fixture, another ran the audit and a third scored it, so shared blind spots could inflate recall. +- The skill fixes from the acme-demo score were in the skills this run used. Fixing skills from this score would end kadamba-fit's blindness, and another blind measure would need a new fixture. +- The run sheet asks for the score and audit output to be committed under `examples/kadamba-fit.run-/`. That wasn't done, because this scoring brief said not to commit. diff --git a/examples/kadamba-fit.run-1/summary.md b/examples/kadamba-fit.run-1/summary.md new file mode 100644 index 0000000..29cdcd9 --- /dev/null +++ b/examples/kadamba-fit.run-1/summary.md @@ -0,0 +1,147 @@ +# Kadamba Fit: full DPDP and GDPR audit + +Assessment `2026-11-05-kadamba-cold-full`, company `kadamba-cold`. Audited `examples/kadamba-fit` at commit `b6d1c44` with Vigil 0.2.0 on Claude Opus 5.5. Law as at 2 November 2026, the "Last verified" date of both catalogs. This was a static read: the code does not run and modules such as `kadamba.models`, `crm`, `offer_feed` and `db` are not in the repository. + +This is compliance tooling, not legal advice. Take binding decisions to qualified counsel. Several findings below need that, and it is urgent for the Art. 9 basis, the US transfer and any breach assessment of past exports. + +## Result + +Overall posture: non-compliant. 40 findings: 9 critical, 15 high, 12 medium, 4 low. + +Before 13 May 2027 most DPDP gaps are readiness gaps, and each finding says so. GDPR and the CERT-In Directions apply today, so the EU findings below are current exposure. The GDPR upper tier caps at EUR 20 million here, because 4% of EUR 6 million turnover is only EUR 240,000. DPDP caps reach Rs 250 crore for s.8(5), and Rs 200 crore each for s.8(6) and s.9, from 13 May 2027. + +The documents are better than the code. The schedule, partner agreement and notice promise deletion, a three-field share and SCCs, and the jobs and config do something else (F-0002, F-0001, F-0007). + +## Fix first + +1. **Stop the Harbourline export** (F-0001). It sends email, date of birth and health averages to an insurer against the agreement and the consent text. +2. **Stop EU analytics to us-east-1** (F-0007, F-0012). No transfer tool, no processor terms, and the Irish controller is not a party. +3. **Turn off middleware DEBUG and stop the staging refresh** (F-0005, F-0004, F-0016). Bodies, tokens and health data sit in logs and in staging with contractor access. +4. **Rotate the committed database credential and fix field encryption** (F-0006, F-0015). +5. **Fix the purge job** (F-0002, F-0019). Health and cycle data are never deleted, and a parent with a dependant stops the whole nightly run. +6. **Give health data a basis and fix children's processing** (F-0003, F-0008, F-0009, F-0010). +7. **Rewrite the breach matrix** (F-0020 to F-0023, F-0026). Art. 33 applies today and the current clock and thresholds would miss it. + +## Findings + +| ID | Severity | Clause | Issue | Location | +|---|---|---|---|---| +| F-0001 | Critical | Art. 5(1)(c), 9(1); s.6(1) | Export to insurer exceeds Schedule A and consent | `jobs/partner_export.py:8` | +| F-0002 | Critical | Art. 5(1)(e), 17(1); s.8(7) | Health and cycle data never erased | `jobs/purge_closed.py:6` | +| F-0003 | Critical | Art. 9(2)(a); s.4, s.6, s.7(a) | No Art. 9(2) condition for health data | `docs/ropa.md:9` | +| F-0004 | Critical | Art. 32(1)(b); s.8(5) | Unmasked production data in staging | `scripts/refresh_staging.sh:6` | +| F-0005 | Critical | Art. 32(1)(a); Rule 6(1)(a) | Headers and bodies in production logs | `api/middleware.py:11` | +| F-0006 | Critical | Art. 32(1)(b); Rule 6(1)(b) | Production DB credential committed | `deploy/secrets.yaml:7` | +| F-0007 | Critical | Art. 44, 46(1) | EU analytics to US with no transfer tool | `config/production.yaml:7` | +| F-0008 | Critical | s.9(3); Art. 25(1) | Children get targeted offers: `is_minor` unset | `services/family.py:6` | +| F-0009 | Critical | s.9(1), Rule 10; Art. 8(2) | No verifiable parental consent | `services/family.py:4` | +| F-0010 | High | Art. 7(4); s.6(1) | Insurer consent forced at signup | `config/app.yaml:14` | +| F-0011 | High | Art. 7(3); s.6(6) | Marketing withdrawal ignored by CRM sync | `jobs/campaign_sync.py:6` | +| F-0012 | High | Art. 28(3); s.8(2) | Quantlytics has no processor terms, own-use grant | `docs/vendors/quantlytics-order-form.md:15` | +| F-0013 | High | Art. 5(1)(b), 6(1); s.4 | Undisclosed sponsored-offer profiling | `services/offers.py:9` | +| F-0014 | High | s.4, s.6, s.9(3) | Analytics: no DPDP ground, tracks children | `services/analytics.py:6` | +| F-0015 | High | Art. 32(1)(a); Rule 6(1)(a) | Coach tax and bank data plaintext as deployed | `utils/fieldcrypt.py:11` | +| F-0016 | High | Art. 25(1); Rule 6(1)(a) | Masking script covers only four columns | `scripts/mask_staging.sql:1` | +| F-0017 | High | Art. 15, 20; s.11 | Export omits health data and recipients | `services/rights.py:4` | +| F-0018 | High | Art. 19; s.8(7) | Erasure and withdrawal do not reach Harbourline | `jobs/purge_closed.py:12` | +| F-0019 | High | s.8(7), s.9; Art. 17 | Dependants left active, foreign key stops purge | `jobs/purge_closed.py:14` | +| F-0020 | High | Art. 33(1); Rule 7(2)(b) | 72 hours counted from declaration | `docs/runbooks/severity-matrix.md:5` | +| F-0021 | High | Rule 7(2)(a) | No initial Board intimation | `docs/runbooks/severity-matrix.md:5` | +| F-0022 | High | Rule 7(1); Art. 34(1) | Member notice optional and after review | `docs/runbooks/severity-matrix.md:5` | +| F-0023 | High | s.8(6); Art. 33(1), (5) | Sev 2 and 3 never notify, count-only severity | `docs/runbooks/severity-matrix.md:6` | +| F-0024 | High | Rule 6(1)(e), 8(3); CERT-In | Logs kept 30 days, schedule says 12 months | `deploy/logging.yaml:9` | +| F-0025 | Medium | Art. 5(1)(c), 6(1)(f), 21 | Email and IP in analytics, no assessment found | `services/analytics.py:9` | +| F-0026 | Medium | s.8(6), Rule 7; Art. 33 | No awareness time or personal data step | `docs/runbooks/incident-response.md:2` | +| F-0027 | Medium | Rule 14(3), s.13(2) | Grievance limit 120 days, period not published | `config/support.yaml:4` | +| F-0028 | Medium | Art. 13(2)(b), 21(4) | Notice omits restriction, portability, objection | `web/privacy-notice.html:29` | +| F-0029 | Medium | Art. 13; Rule 3(b) | Notice omits data, recipients, retention, coaches | `web/privacy-notice.html:10` | +| F-0030 | Medium | Art. 12(1); s.5(3) | No German notice, no Eighth Schedule option | `web/privacy-notice.html:5` | +| F-0031 | Medium | Art. 30(1) | RoPA lacks fields and activities | `docs/ropa.md:5` | +| F-0032 | Medium | Art. 28(1), 30(1)(d); s.8(2) | Recipients missing from vendor register | `docs/vendors/register.md:3` | +| F-0033 | Medium | Art. 5(2), 7(1); s.6(10) | Consent proof can go before 3 years after closure | `jobs/purge_closed.py:24` | +| F-0034 | Medium | Art. 5(1)(e); s.8(7) | No period for coach data, dormant accounts, raw samples | `docs/retention-schedule.md:5` | +| F-0035 | Medium | Art. 35(1) | DPIA not reviewed, other processing not covered | `README.md:19` | +| F-0036 | Medium | Art. 16, 19; s.12 | Corrections not sent on, dob edit skips age rules | `services/rights.py:19` | +| F-0037 | Low | Art. 6(3) | Irish invoices justified by Indian statutes | `docs/retention-schedule.md:9` | +| F-0038 | Low | Art. 7(3), 12(2); s.6(4) | Settings hides consent state, no erasure link | `web/settings.html:3` | +| F-0039 | Low | Art. 5(1)(c); s.6(1) | Cycle notes and device serial have no stated need | `services/cycle.py:6` | +| F-0040 | Low | Art. 13(2)(f), 22 | Open question: is the tier an automated decision? | `docs/vendors/harbourline-partner-agreement.md:5` | + +## Steps run + +Every step ran except the sector check. The sub-skills were read and applied inline in this one run. + +| Step | Skill | Outcome | +|---|---|---| +| 1 | `/pii-scan` | All 52 files in the app folder read. Fixture checked, no finding (see below) | +| 2 | `/gdpr-data-flows` | 22 flows in `data-map.json` | +| 3 | `/dpdp-consent` | F-0008 to F-0011, F-0014, F-0030 | +| 4 | `/dpdp-rights audit` | F-0017, F-0018, F-0027, F-0028, F-0036, F-0038 | +| 5 | `/dpdp-dpia screen` | Art. 35 triggers met (health, children, monitoring, profiling). s.10(2)(c) does not apply: not an SDF. Draft written for the processing outside the reported DPIA's stated scope: `company-profile/kadamba-cold/dpias/dpia-2026-11-partner-children-offers.json` | +| 6 | `/dpdp-breach readiness` | F-0020 to F-0024, F-0026. Nothing written to the breach log | +| 7 | Sector check | **Skipped: not triggered.** `details.json` lists no RBI, SEBI or IRDAI and the company is not financial. The partner is an insurer, so IRDAI is left as an unknown | +| 8 | `/gdpr-audit` | Security, retention and basis findings | +| 9 | `/gdpr-draft review` | Table below | +| 10 | `/gdpr-audit transfers` | F-0007. Other transfers credited below | +| 11 | `/posture` | `posture.json`, overall non-compliant | +| 12 | Report | `report.json` | + +## Document review + +| Document | Status | Note | +|---|---|---| +| Privacy notice | Incomplete | F-0007, F-0028, F-0029, F-0030 | +| Signup and consent text | Incomplete | F-0010 | +| Mailwren DPA | Present | Covers the Art. 28(3) terms | +| Nimbusline terms | Present | Covers the duties. It does not list data types or categories of people, which I did not raise as a finding on this excerpt | +| Quantlytics order form | Incomplete | F-0012 | +| Harbourline agreement | Present | Clear and limited. The code does not follow it (F-0001) | +| Cadencehive DPA | Not found in the reviewed material | Register says signed 2 November 2024, held by Legal | +| Intra-group agreement, SCCs, transfer impact assessment | Not found in the reviewed material | Listed in the register | +| Retention schedule | Incomplete | F-0034, F-0037 | +| Rights procedure | Not found in the reviewed material | Only `config/support.yaml` and the services | +| DPIA | Not found in the reviewed material | README says the DPO holds it | +| Legitimate interests assessment | Not found in the reviewed material | F-0025 | +| Incident runbook and severity matrix | Incomplete | F-0020 to F-0023, F-0026 | + +## Checked and no finding + +- `tests/fixtures/members.json`: reserved `example.test` addresses, zero-filled phone numbers and placeholder names. These cannot be real, so I did not treat them as realistic-format identifiers. +- `config/env.example` holds only placeholders. `production.yaml` and the CRM key use references. +- Consent ledger: append-only design, notice version and source recorded, latest row wins. The reminder and partner jobs read it correctly. +- Signup boxes are not pre-ticked. One switch per purpose in Settings. +- OTP: `secrets` module, 5-minute TTL, masked phone in logs. `auth_sessions` stores hashes. Payment data is a PSP token, brand and last four digits only. +- `rights_request` at 600 hours (25 days) is inside one month. `first_response` matches the 48-hour house standard. +- Invoices surviving erasure, and consent events kept after closure, are allowed in principle. Only the Irish basis (F-0037) and the date used (F-0033) are raised. +- Transfers: India disaster recovery in Singapore is allowed under s.16 with no restriction in the catalog. EU disaster recovery stays in Dublin. Mailwren Inc. access runs under SCC Module 3 with a transfer impact assessment. Bengaluru support is listed under SCC Module 2 with one. +- DPO based in Dublin: the India-based DPO rule in s.10(2)(a) binds only an SDF. +- Snapshots at 35 days match the schedule and are encrypted per cluster. +- CERT-In 6-hour step and evidence preservation are in the runbook. Nomination and grievance routes exist. The notice has the Board and supervisory authority routes. + +## Could not verify + +- That the "synthetic" database credential and the README's claims about it are true (F-0006). +- Whether a key reaches `FIELDCRYPT_KEY` outside the manifests (F-0015). +- Which events call `track()`, what `offer_feed` does with a segment, and how `activity_tier` is derived. No code for these is in the repository. +- Staging's location, contractors' location, and whether the database connection uses TLS (the URL sets no `sslmode`). +- Whether a plan exists for the s.5(2) notice to members who consented before commencement. +- Cross-border status of the CRM, SMS, push, payment and ad partner flows. + +## Controls + +Counted at top-level section, rule or article. The denominator is my own count from this organisation's facts, so do not compare it across runs. + +**Applicable: 51. Examined: 45.** + +- **DPDP sections (11, all examined):** s.4, 5, 6, 7, 8, 9, 11, 12, 13, 14, 16. +- **DPDP rules (10, 9 examined):** Rules 3, 6, 7, 8, 9, 10, 12, 14, 15. Not examined: Rule 11 (no material on guardians of persons with disability). Rule 12 and Rule 15 and s.16 were checked against the catalog only: no exemption or country restriction is claimed or listed. +- **GDPR (30, 25 examined):** Art. 5, 6, 7, 8, 9, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 25, 28, 30, 32, 33, 34, 35, 37, 44, 46. Not examined: Art. 14 (indirect collection, such as nominee and dependant data), 24 (policies), 36 (needs the DPIA), 38 and 39 (DPO position and tasks). +- **Related instruments checked, not counted:** CERT-In Directions 2022, CCPA Dark Patterns Guidelines 2023. +- **Excluded as not applicable:** s.10 and Rule 13 (not designated an SDF), Rule 4 (no Consent Manager in the material), Rules 5 and 16, Art. 10, 26, 27, 45, 47, 49. + +## Schema stand-ins + +- `details.json`: `legal_name` and `registered_address` hold two entities in one string. `dpo` holds only the mailbox. +- `data-map.json`: `lawful_basis` takes one value, so claimed bases that the evidence does not support are `none_identified` with the claim in `notes`. Basis values for backup and support flows follow the source data. DF-22 uses `legal_obligation` as a stand-in. `transfer_mechanism` has no DPDP value, so India flows use `null` and explain it in `notes`. +- `report.json`: one `file` and `line` per finding, so other locations are in the issue text. SARIF shows only the first. +- CERT-In and the CCPA guidelines appear only in `cross_reference`, because `framework` takes DPDP, GDPR or UK-GDPR. diff --git a/package.json b/package.json index 0080e43..9f49219 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vigil", - "version": "0.2.0", + "version": "0.3.0", "private": true, "description": "Open-source DPDP and GDPR compliance workspace agent: 20 Claude Code skills, JSON Schemas and write-guard hooks", "license": "Apache-2.0",