Flamki commited on
Commit
0ecd46c
·
verified ·
1 Parent(s): 5b3e969

Add DPDP policy control matrix

Browse files
Files changed (4) hide show
  1. __pycache__/server.cpython-310.pyc +2 -2
  2. dashboard.css +14 -0
  3. dashboard.js +13 -0
  4. server.py +264 -17
__pycache__/server.cpython-310.pyc CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:5df4d6fb8be71cc7c0fbc66169cb50ec19bca9c4c2c025fb5f5dfa8bd6bd45eb
3
- size 145960
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:13079c3489f54c20fc3d1d8607ac76848be9a8065fae61fbe98b229b29782a62
3
+ size 155172
dashboard.css CHANGED
@@ -1180,6 +1180,14 @@ body.theme-light .profile-submenu .profile-dropdown__item { color: var(--text-pr
1180
  margin-bottom: 16px;
1181
  }
1182
 
 
 
 
 
 
 
 
 
1183
  .tool-score strong {
1184
  color: var(--text-primary);
1185
  font: 800 30px/1 'JetBrains Mono', monospace;
@@ -1202,6 +1210,12 @@ body.theme-light .profile-submenu .profile-dropdown__item { color: var(--text-pr
1202
  margin-top: 16px;
1203
  }
1204
 
 
 
 
 
 
 
1205
  .tool-result-item {
1206
  padding: 14px;
1207
  border: 1px solid rgba(255,255,255,0.08);
 
1180
  margin-bottom: 16px;
1181
  }
1182
 
1183
+ .tool-score--compact {
1184
+ padding: 14px 16px;
1185
+ }
1186
+
1187
+ .tool-score--compact strong {
1188
+ font-size: 22px;
1189
+ }
1190
+
1191
  .tool-score strong {
1192
  color: var(--text-primary);
1193
  font: 800 30px/1 'JetBrains Mono', monospace;
 
1210
  margin-top: 16px;
1211
  }
1212
 
1213
+ .tool-section-title {
1214
+ color: var(--text-primary);
1215
+ font-size: 16px;
1216
+ margin: 4px 0 12px;
1217
+ }
1218
+
1219
  .tool-result-item {
1220
  padding: 14px;
1221
  border: 1px solid rgba(255,255,255,0.08);
dashboard.js CHANGED
@@ -1641,11 +1641,23 @@ function initProductTools() {
1641
  };
1642
  const evidenceLine = check => (check.evidence || []).slice(0, 2).map(e => `<span>${escapeFlowHtml(e)}</span>`).join('');
1643
  const processors = (data.detected_processors || []).slice(0, 8).map(p => `<span class="badge badge--warning">${escapeFlowHtml(p.name || 'Provider')} · ${escapeFlowHtml(p.category || 'processor')}</span>`).join('');
 
 
 
 
 
 
 
 
1644
  render(out, `
1645
  <div class="tool-score">
1646
  <strong>${escapeFlowHtml(data.grade)}</strong>
1647
  <span>${escapeFlowHtml(data.verdict)} · ${escapeFlowHtml(data.score)}% · ${escapeFlowHtml(data.overall_risk || 'Review')} risk</span>
1648
  </div>
 
 
 
 
1649
  ${processors ? `<div class="tool-result-item"><strong>Detected processors / providers</strong><span>${processors}</span></div>` : ''}
1650
  <div class="tool-grid">${(data.checks || []).map(c => `
1651
  <div class="tool-result-item">
@@ -1655,6 +1667,7 @@ function initProductTools() {
1655
  <span>${escapeFlowHtml(c.section || '')}</span>
1656
  </div>`).join('')}</div>
1657
  <div class="tool-result-list">
 
1658
  <div class="tool-result-item"><strong>Priority actions</strong>${(data.priority_actions || []).slice(0, 4).map(a => `<span>${escapeFlowHtml(a)}</span>`).join('')}</div>
1659
  <div class="tool-result-item"><strong>Scanner limitations</strong>${(data.limitations || []).map(a => `<span>${escapeFlowHtml(a)}</span>`).join('')}</div>
1660
  </div>
 
1641
  };
1642
  const evidenceLine = check => (check.evidence || []).slice(0, 2).map(e => `<span>${escapeFlowHtml(e)}</span>`).join('');
1643
  const processors = (data.detected_processors || []).slice(0, 8).map(p => `<span class="badge badge--warning">${escapeFlowHtml(p.name || 'Provider')} · ${escapeFlowHtml(p.category || 'processor')}</span>`).join('');
1644
+ const policy = data.policy_control_matrix || {};
1645
+ const policyControls = (policy.controls || []).slice(0, 12).map(c => `
1646
+ <div class="tool-result-item">
1647
+ <strong>${statusBadge(c.status)} ${escapeFlowHtml(c.title || c.id)}</strong>
1648
+ <span>${escapeFlowHtml(c.act || '')}</span>
1649
+ <span>${escapeFlowHtml(c.rules || '')}</span>
1650
+ ${(c.evidence || []).slice(0, 1).map(e => `<span>${escapeFlowHtml(e)}</span>`).join('')}
1651
+ </div>`).join('');
1652
  render(out, `
1653
  <div class="tool-score">
1654
  <strong>${escapeFlowHtml(data.grade)}</strong>
1655
  <span>${escapeFlowHtml(data.verdict)} · ${escapeFlowHtml(data.score)}% · ${escapeFlowHtml(data.overall_risk || 'Review')} risk</span>
1656
  </div>
1657
+ <div class="tool-score tool-score--compact">
1658
+ <strong>${escapeFlowHtml(policy.grade || '-')}</strong>
1659
+ <span>Policy evidence · ${escapeFlowHtml(policy.score ?? 'n/a')}% · ${escapeFlowHtml((policy.coverage && `${policy.coverage.pass || 0} pass / ${policy.coverage.review || 0} review / ${policy.coverage.fail || 0} fail`) || 'no policy matrix')}</span>
1660
+ </div>
1661
  ${processors ? `<div class="tool-result-item"><strong>Detected processors / providers</strong><span>${processors}</span></div>` : ''}
1662
  <div class="tool-grid">${(data.checks || []).map(c => `
1663
  <div class="tool-result-item">
 
1667
  <span>${escapeFlowHtml(c.section || '')}</span>
1668
  </div>`).join('')}</div>
1669
  <div class="tool-result-list">
1670
+ ${policyControls ? `<div><h3 class="tool-section-title">Policy Control Matrix</h3><div class="tool-grid">${policyControls}</div></div>` : ''}
1671
  <div class="tool-result-item"><strong>Priority actions</strong>${(data.priority_actions || []).slice(0, 4).map(a => `<span>${escapeFlowHtml(a)}</span>`).join('')}</div>
1672
  <div class="tool-result-item"><strong>Scanner limitations</strong>${(data.limitations || []).map(a => `<span>${escapeFlowHtml(a)}</span>`).join('')}</div>
1673
  </div>
server.py CHANGED
@@ -3293,6 +3293,191 @@ def _simple_grade(score: int) -> str:
3293
  return "F"
3294
 
3295
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3296
  def _dpdp_quick_from_report(report: dict, service_map: Optional[dict] = None) -> dict:
3297
  compliance = report.get("compliance", {}) or {}
3298
  dpdp = report.get("dpdp", {}) or {}
@@ -3303,6 +3488,8 @@ def _dpdp_quick_from_report(report: dict, service_map: Optional[dict] = None) ->
3303
  exposed = report.get("exposed_pii", {}) or {}
3304
  headers = compliance.get("security_headers", {}) or {}
3305
  blacklight = report.get("blacklight", {}) or {}
 
 
3306
  service_map = service_map or {}
3307
  services = service_map.get("services", []) or []
3308
  api_calls = service_map.get("api_calls", []) or []
@@ -3331,6 +3518,23 @@ def _dpdp_quick_from_report(report: dict, service_map: Optional[dict] = None) ->
3331
  def header_pass(name: str) -> bool:
3332
  return bool((headers.get(name) or {}).get("present")) and (headers.get(name) or {}).get("rating") in {"pass", "warn"}
3333
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3334
  def make_check(
3335
  check_id: str,
3336
  label: str,
@@ -3360,20 +3564,21 @@ def _dpdp_quick_from_report(report: dict, service_map: Optional[dict] = None) ->
3360
  checks = []
3361
 
3362
  privacy_url = compliance.get("privacy_policy_url")
 
3363
  checks.append(make_check(
3364
  "privacy_notice",
3365
- "Privacy notice is discoverable",
3366
- "pass" if compliance.get("privacy_policy") and privacy_url else "fail",
3367
  "DPDP Act Section 5 - Notice",
3368
  "high",
3369
  "A Data Principal should be able to find a clear notice explaining what personal data is collected and why.",
3370
- "Publish a visible privacy notice link in the footer, signup, checkout, and every form that collects personal data.",
3371
- [f"Privacy URL: {privacy_url}" if privacy_url else "No privacy policy URL discovered on the scanned page"],
3372
  16,
3373
  "high" if privacy_url else "medium",
3374
  ))
3375
 
3376
- consent_ready = bool(compliance.get("cookie_consent")) and dpdp_pass("consent_mechanism")
3377
  consent_status = "pass" if consent_ready else "fail" if (tracker_count or non_essential_processors) else "review"
3378
  checks.append(make_check(
3379
  "consent_mechanism",
@@ -3395,44 +3600,56 @@ def _dpdp_quick_from_report(report: dict, service_map: Optional[dict] = None) ->
3395
  checks.append(make_check(
3396
  "consent_withdrawal",
3397
  "Consent withdrawal or preference management is disclosed",
3398
- "pass" if dpdp_pass("consent_withdrawal") else "fail" if (tracker_count or non_essential_processors) else "review",
3399
  "DPDP Act Section 6(4) - Consent withdrawal",
3400
  "high",
3401
  "Users should have an easy way to withdraw consent or change preferences after accepting.",
3402
  "Add a persistent cookie/privacy preferences link and describe withdrawal steps in the privacy notice.",
3403
- ["Policy text matched withdrawal language" if dpdp_pass("consent_withdrawal") else "No withdrawal/preference evidence found in public text"],
3404
  12,
3405
  ))
3406
 
3407
  checks.append(make_check(
3408
  "grievance_redressal",
3409
  "Grievance/contact path is published",
3410
- "pass" if dpdp_pass("grievance_officer") else "fail",
3411
  "DPDP Act Section 13 and Section 8(7) - Grievance redressal",
3412
  "high",
3413
  "A user should know who to contact for privacy complaints, correction, erasure, and escalation.",
3414
  "Add a privacy contact or grievance officer section with email, response process, and escalation path.",
3415
- ["Grievance/DPO language found" if dpdp_pass("grievance_officer") else "No grievance officer, DPO, privacy@, or redressal signal found"],
3416
  12,
3417
  ))
3418
 
3419
  checks.append(make_check(
3420
  "retention_and_erasure",
3421
  "Retention, deletion, correction, and erasure rights are disclosed",
3422
- "pass" if dpdp_pass("data_retention_policy") else "fail",
3423
  "DPDP Act Section 8(6), Section 11, and Section 12",
3424
  "high",
3425
  "Users should know how long data is kept and how they can request correction or deletion.",
3426
  "Document retention periods by data category and add correction/deletion request instructions.",
3427
- ["Retention/deletion language found" if dpdp_pass("data_retention_policy") else "No retention period, deletion, or erasure evidence found"],
3428
  12,
3429
  ))
3430
 
 
 
 
 
 
 
 
 
 
 
 
 
3431
  vendor_status = "pass" if processor_count == 0 and tracker_count == 0 else "review"
3432
  checks.append(make_check(
3433
  "processor_inventory",
3434
  "Third-party processors and API destinations are inventoried",
3435
- vendor_status,
3436
  "DPDP Act Section 8 - Data Fiduciary accountability",
3437
  "medium" if vendor_status == "review" else "low",
3438
  "Every analytics, database, payment, support, AI, and auth provider needs a purpose, data category, contract owner, and retention note.",
@@ -3440,11 +3657,24 @@ def _dpdp_quick_from_report(report: dict, service_map: Optional[dict] = None) ->
3440
  [
3441
  f"Detected processors/providers: {', '.join(sorted({p.get('name') for p in detected_processors if p.get('name')}))}" if detected_processors else "No third-party processor signal detected",
3442
  f"API calls discovered: {len(api_calls)}" if api_calls else "",
3443
- ],
3444
  12,
3445
  "medium" if processor_count else "high",
3446
  ))
3447
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3448
  checks.append(make_check(
3449
  "public_pii_exposure",
3450
  "No obvious personal data is exposed on public pages",
@@ -3462,7 +3692,7 @@ def _dpdp_quick_from_report(report: dict, service_map: Optional[dict] = None) ->
3462
  checks.append(make_check(
3463
  "secure_transport_headers",
3464
  "HTTPS and browser privacy/security headers are configured",
3465
- "pass" if browser_security_ok else "fail",
3466
  "DPDP Act Section 8(5) - Reasonable security safeguards",
3467
  "medium",
3468
  "Transport security and browser headers reduce accidental disclosure and client-side abuse.",
@@ -3471,10 +3701,22 @@ def _dpdp_quick_from_report(report: dict, service_map: Optional[dict] = None) ->
3471
  f"HTTPS: {bool(compliance.get('https'))}",
3472
  f"HSTS: {(headers.get('strict-transport-security') or {}).get('note', 'missing')}",
3473
  f"Referrer-Policy: {(headers.get('referrer-policy') or {}).get('note', 'missing')}",
3474
- ],
3475
  8,
3476
  ))
3477
 
 
 
 
 
 
 
 
 
 
 
 
 
3478
  child_status = "pass" if dpdp_pass("children_protection") else "review"
3479
  checks.append(make_check(
3480
  "children_data",
@@ -3530,10 +3772,12 @@ def _dpdp_quick_from_report(report: dict, service_map: Optional[dict] = None) ->
3530
  "manual_review": review[:4],
3531
  "detected_processors": detected_processors,
3532
  "evidence_register": evidence_register,
 
3533
  "plain_english": [
3534
  f"We found {pii.get('pii_input_count', 0)} personal-data input signal(s).",
3535
  f"We found {tracker_count} tracker signal(s) and {processor_count} processor/provider signal(s).",
3536
  f"Public PII exposure count: {exposed.get('count', 0)}.",
 
3537
  f"DPDP text-evidence checks passed: {dpdp.get('score', 'not available')} of {dpdp.get('total_checks', 'unknown')}.",
3538
  ],
3539
  "priority_actions": [
@@ -4345,8 +4589,10 @@ async def scan_url(req: URLScanRequest):
4345
  except Exception as e:
4346
  print(f"[!] Privacy policy page fetch failed: {e}")
4347
 
4348
- # Combine homepage + privacy page for DPDP analysis
4349
- combined_compliance_text = page_lower + " " + privacy_page_text
 
 
4350
 
4351
  # ---- 15. DPDP ACT 2023 (India) COMPLIANCE CHECKS ----
4352
  # Based on the Digital Personal Data Protection Act, 2023
@@ -4644,6 +4890,7 @@ async def scan_url(req: URLScanRequest):
4644
  "https": is_https,
4645
  "privacy_policy": has_privacy_policy,
4646
  "privacy_policy_url": privacy_url_found,
 
4647
  "cookie_consent": has_cookie_consent,
4648
  "cmp_platforms": cmp_detected,
4649
  "terms_of_service": has_terms,
 
3293
  return "F"
3294
 
3295
 
3296
+ DPDP_POLICY_CONTROL_CATALOG = [
3297
+ {
3298
+ "id": "notice_plain_language",
3299
+ "title": "Standalone notice in clear language",
3300
+ "act": "DPDP Act Section 5",
3301
+ "rules": "DPDP Rules 2025 - notice requirements",
3302
+ "keywords": ["privacy notice", "privacy policy", "personal data", "collect", "purpose", "processing"],
3303
+ "required_hits": 3,
3304
+ "weight": 10,
3305
+ "fix": "Make the notice standalone, plain-English, and separate from unrelated terms.",
3306
+ },
3307
+ {
3308
+ "id": "itemized_personal_data",
3309
+ "title": "Itemized personal data categories",
3310
+ "act": "DPDP Act Section 5",
3311
+ "rules": "DPDP Rules 2025 - itemized data collection notice",
3312
+ "keywords": ["name", "email", "phone", "address", "payment", "device", "location", "personal information", "personal data we collect"],
3313
+ "required_hits": 2,
3314
+ "weight": 9,
3315
+ "fix": "List each data category collected, grouped by product workflow.",
3316
+ },
3317
+ {
3318
+ "id": "purpose_specificity",
3319
+ "title": "Specific purpose for each collection",
3320
+ "act": "DPDP Act Section 5 and Section 7",
3321
+ "rules": "DPDP Rules 2025 - purpose description in notice",
3322
+ "keywords": ["purpose", "to provide", "to process", "to improve", "to communicate", "for marketing", "for analytics", "services enabled"],
3323
+ "required_hits": 2,
3324
+ "weight": 10,
3325
+ "fix": "Map every data category to a specific purpose and service enabled by processing.",
3326
+ },
3327
+ {
3328
+ "id": "consent_withdrawal",
3329
+ "title": "Consent withdrawal and preference management",
3330
+ "act": "DPDP Act Section 6(4)",
3331
+ "rules": "DPDP Rules 2025 - withdrawal comparable to giving consent",
3332
+ "keywords": ["withdraw consent", "revoke consent", "manage consent", "manage preferences", "cookie settings", "opt out", "unsubscribe"],
3333
+ "required_hits": 1,
3334
+ "weight": 12,
3335
+ "fix": "Provide a persistent preference link and make withdrawal as easy as giving consent.",
3336
+ },
3337
+ {
3338
+ "id": "data_principal_rights",
3339
+ "title": "Access, correction, erasure, grievance, and nomination rights",
3340
+ "act": "DPDP Act Sections 11, 12, 13, and 14",
3341
+ "rules": "DPDP Rules 2025 - rights request handling",
3342
+ "keywords": ["access your data", "correct", "correction", "erase", "erasure", "delete your data", "grievance", "complaint", "nominate"],
3343
+ "required_hits": 3,
3344
+ "weight": 13,
3345
+ "fix": "Add a rights section covering access, correction, deletion, grievance, and nomination workflows.",
3346
+ },
3347
+ {
3348
+ "id": "grievance_contact",
3349
+ "title": "Grievance/contact channel and escalation",
3350
+ "act": "DPDP Act Section 13 and Section 8(7)",
3351
+ "rules": "DPDP Rules 2025 - complaint and Board communication link",
3352
+ "keywords": ["grievance officer", "grievance", "privacy@", "dpo@", "data protection officer", "complaint", "data protection board"],
3353
+ "required_hits": 1,
3354
+ "weight": 12,
3355
+ "fix": "Publish a privacy contact, grievance process, response path, and escalation route.",
3356
+ },
3357
+ {
3358
+ "id": "retention_deletion",
3359
+ "title": "Retention schedule and deletion policy",
3360
+ "act": "DPDP Act Section 8(6)",
3361
+ "rules": "DPDP Rules 2025 - retention/deletion accountability",
3362
+ "keywords": ["retention", "retain", "how long", "delete", "deletion", "erasure", "storage period", "no longer necessary"],
3363
+ "required_hits": 2,
3364
+ "weight": 12,
3365
+ "fix": "State retention periods by data category and explain deletion triggers.",
3366
+ },
3367
+ {
3368
+ "id": "security_safeguards",
3369
+ "title": "Reasonable security safeguards",
3370
+ "act": "DPDP Act Section 8(5)",
3371
+ "rules": "DPDP Rules 2025 - security safeguards and breach duties",
3372
+ "keywords": ["security", "safeguards", "encryption", "access control", "confidentiality", "incident", "breach", "unauthorized"],
3373
+ "required_hits": 2,
3374
+ "weight": 10,
3375
+ "fix": "Describe encryption, access controls, audit logging, incident response, and vendor security safeguards.",
3376
+ },
3377
+ {
3378
+ "id": "breach_notification",
3379
+ "title": "Breach notification process",
3380
+ "act": "DPDP Act Section 8(6) and security obligations",
3381
+ "rules": "DPDP Rules 2025 - personal data breach notice",
3382
+ "keywords": ["data breach", "security breach", "personal data breach", "notify", "notification", "incident response", "data protection board"],
3383
+ "required_hits": 2,
3384
+ "weight": 9,
3385
+ "fix": "Document breach notification to affected users and the Data Protection Board.",
3386
+ },
3387
+ {
3388
+ "id": "children_data",
3389
+ "title": "Children's data posture",
3390
+ "act": "DPDP Act Section 9",
3391
+ "rules": "DPDP Rules 2025 - verifiable parental consent",
3392
+ "keywords": ["child", "children", "minor", "under 18", "parental consent", "guardian", "age verification"],
3393
+ "required_hits": 1,
3394
+ "weight": 7,
3395
+ "fix": "State whether children can use the service; if yes, document parental consent and no tracking/profiling controls.",
3396
+ },
3397
+ {
3398
+ "id": "processor_vendor_sharing",
3399
+ "title": "Processors, vendors, and sharing purposes",
3400
+ "act": "DPDP Act Section 8 - accountability",
3401
+ "rules": "DPDP Rules 2025 - fiduciary accountability",
3402
+ "keywords": ["third party", "service provider", "processor", "vendor", "affiliate", "share", "sub-processor", "analytics"],
3403
+ "required_hits": 2,
3404
+ "weight": 10,
3405
+ "fix": "List processor categories, sharing purposes, safeguards, and contract ownership.",
3406
+ },
3407
+ {
3408
+ "id": "cross_border_transfer",
3409
+ "title": "Cross-border transfer disclosure",
3410
+ "act": "DPDP Act Section 16",
3411
+ "rules": "DPDP Rules 2025 - cross-border transfer restrictions",
3412
+ "keywords": ["transfer", "outside india", "cross-border", "international", "global", "countries", "jurisdiction"],
3413
+ "required_hits": 1,
3414
+ "weight": 6,
3415
+ "fix": "Disclose whether personal data is transferred outside India and how transfer restrictions are handled.",
3416
+ },
3417
+ ]
3418
+
3419
+
3420
+ def _snippet_for_keywords(text: str, keywords: list, max_snippets: int = 3) -> list:
3421
+ snippets = []
3422
+ if not text:
3423
+ return snippets
3424
+ compact = re.sub(r"\s+", " ", text).strip()
3425
+ lower = compact.lower()
3426
+ for keyword in keywords:
3427
+ idx = lower.find(keyword.lower())
3428
+ if idx == -1:
3429
+ continue
3430
+ start = max(0, idx - 90)
3431
+ end = min(len(compact), idx + len(keyword) + 140)
3432
+ snippet = compact[start:end].strip()
3433
+ if snippet and snippet not in snippets:
3434
+ snippets.append(snippet)
3435
+ if len(snippets) >= max_snippets:
3436
+ break
3437
+ return snippets
3438
+
3439
+
3440
+ def _analyze_dpdp_policy_text(policy_text: str, privacy_url: Optional[str] = None) -> dict:
3441
+ text = re.sub(r"\s+", " ", (policy_text or "")).strip()
3442
+ lower = text.lower()
3443
+ controls = []
3444
+ total_weight = sum(control["weight"] for control in DPDP_POLICY_CONTROL_CATALOG)
3445
+ earned = 0
3446
+ for control in DPDP_POLICY_CONTROL_CATALOG:
3447
+ matched = sorted({kw for kw in control["keywords"] if kw.lower() in lower})
3448
+ passed = len(matched) >= control["required_hits"]
3449
+ partial = bool(matched) and not passed
3450
+ if passed:
3451
+ earned += control["weight"]
3452
+ elif partial:
3453
+ earned += control["weight"] * 0.35
3454
+ snippets = _snippet_for_keywords(text, matched or control["keywords"], max_snippets=3)
3455
+ controls.append({
3456
+ "id": control["id"],
3457
+ "title": control["title"],
3458
+ "status": "pass" if passed else "review" if partial else "fail",
3459
+ "act": control["act"],
3460
+ "rules": control["rules"],
3461
+ "matched_terms": matched[:8],
3462
+ "evidence": snippets,
3463
+ "weight": control["weight"],
3464
+ "fix": control["fix"],
3465
+ })
3466
+ score = round((earned / total_weight) * 100) if total_weight else 0
3467
+ return {
3468
+ "privacy_url": privacy_url,
3469
+ "text_length": len(text),
3470
+ "score": score,
3471
+ "grade": _simple_grade(score),
3472
+ "controls": controls,
3473
+ "coverage": {
3474
+ "pass": sum(1 for c in controls if c["status"] == "pass"),
3475
+ "review": sum(1 for c in controls if c["status"] == "review"),
3476
+ "fail": sum(1 for c in controls if c["status"] == "fail"),
3477
+ },
3478
+ }
3479
+
3480
+
3481
  def _dpdp_quick_from_report(report: dict, service_map: Optional[dict] = None) -> dict:
3482
  compliance = report.get("compliance", {}) or {}
3483
  dpdp = report.get("dpdp", {}) or {}
 
3488
  exposed = report.get("exposed_pii", {}) or {}
3489
  headers = compliance.get("security_headers", {}) or {}
3490
  blacklight = report.get("blacklight", {}) or {}
3491
+ policy_analysis = compliance.get("policy_analysis", {}) or {}
3492
+ policy_controls = {control.get("id"): control for control in (policy_analysis.get("controls") or [])}
3493
  service_map = service_map or {}
3494
  services = service_map.get("services", []) or []
3495
  api_calls = service_map.get("api_calls", []) or []
 
3518
  def header_pass(name: str) -> bool:
3519
  return bool((headers.get(name) or {}).get("present")) and (headers.get(name) or {}).get("rating") in {"pass", "warn"}
3520
 
3521
+ def policy_status(control_id: str) -> str:
3522
+ return (policy_controls.get(control_id) or {}).get("status", "fail")
3523
+
3524
+ def policy_pass(control_id: str) -> bool:
3525
+ return policy_status(control_id) == "pass"
3526
+
3527
+ def policy_evidence(control_id: str, fallback: str) -> list:
3528
+ control = policy_controls.get(control_id) or {}
3529
+ snippets = control.get("evidence") or []
3530
+ terms = control.get("matched_terms") or []
3531
+ evidence = snippets[:2]
3532
+ if terms:
3533
+ evidence.append("Matched terms: " + ", ".join(terms[:6]))
3534
+ if not evidence:
3535
+ evidence.append(fallback)
3536
+ return evidence
3537
+
3538
  def make_check(
3539
  check_id: str,
3540
  label: str,
 
3564
  checks = []
3565
 
3566
  privacy_url = compliance.get("privacy_policy_url")
3567
+ notice_quality = policy_pass("notice_plain_language") and policy_pass("itemized_personal_data") and policy_pass("purpose_specificity")
3568
  checks.append(make_check(
3569
  "privacy_notice",
3570
+ "Privacy notice is discoverable and meaningfully itemized",
3571
+ "pass" if compliance.get("privacy_policy") and privacy_url and notice_quality else "review" if compliance.get("privacy_policy") and privacy_url else "fail",
3572
  "DPDP Act Section 5 - Notice",
3573
  "high",
3574
  "A Data Principal should be able to find a clear notice explaining what personal data is collected and why.",
3575
+ "Publish a visible privacy notice and itemize personal data categories, purpose, goods/services enabled, rights, and complaint links.",
3576
+ [f"Privacy URL: {privacy_url}" if privacy_url else "No privacy policy URL discovered on the scanned page"] + policy_evidence("notice_plain_language", "No clear notice evidence found")[:1],
3577
  16,
3578
  "high" if privacy_url else "medium",
3579
  ))
3580
 
3581
+ consent_ready = bool(compliance.get("cookie_consent")) and (dpdp_pass("consent_mechanism") or policy_pass("consent_withdrawal"))
3582
  consent_status = "pass" if consent_ready else "fail" if (tracker_count or non_essential_processors) else "review"
3583
  checks.append(make_check(
3584
  "consent_mechanism",
 
3600
  checks.append(make_check(
3601
  "consent_withdrawal",
3602
  "Consent withdrawal or preference management is disclosed",
3603
+ "pass" if dpdp_pass("consent_withdrawal") or policy_pass("consent_withdrawal") else "fail" if (tracker_count or non_essential_processors) else "review",
3604
  "DPDP Act Section 6(4) - Consent withdrawal",
3605
  "high",
3606
  "Users should have an easy way to withdraw consent or change preferences after accepting.",
3607
  "Add a persistent cookie/privacy preferences link and describe withdrawal steps in the privacy notice.",
3608
+ policy_evidence("consent_withdrawal", "No withdrawal/preference evidence found in public text"),
3609
  12,
3610
  ))
3611
 
3612
  checks.append(make_check(
3613
  "grievance_redressal",
3614
  "Grievance/contact path is published",
3615
+ "pass" if dpdp_pass("grievance_officer") or policy_pass("grievance_contact") else "fail",
3616
  "DPDP Act Section 13 and Section 8(7) - Grievance redressal",
3617
  "high",
3618
  "A user should know who to contact for privacy complaints, correction, erasure, and escalation.",
3619
  "Add a privacy contact or grievance officer section with email, response process, and escalation path.",
3620
+ policy_evidence("grievance_contact", "No grievance officer, DPO, privacy@, or redressal signal found"),
3621
  12,
3622
  ))
3623
 
3624
  checks.append(make_check(
3625
  "retention_and_erasure",
3626
  "Retention, deletion, correction, and erasure rights are disclosed",
3627
+ "pass" if dpdp_pass("data_retention_policy") and policy_pass("data_principal_rights") else "review" if dpdp_pass("data_retention_policy") or policy_pass("data_principal_rights") or policy_pass("retention_deletion") else "fail",
3628
  "DPDP Act Section 8(6), Section 11, and Section 12",
3629
  "high",
3630
  "Users should know how long data is kept and how they can request correction or deletion.",
3631
  "Document retention periods by data category and add correction/deletion request instructions.",
3632
+ policy_evidence("retention_deletion", "No retention period, deletion, or erasure evidence found") + policy_evidence("data_principal_rights", "")[:1],
3633
  12,
3634
  ))
3635
 
3636
+ checks.append(make_check(
3637
+ "rights_request_workflow",
3638
+ "Data Principal rights workflow is operationally described",
3639
+ "pass" if policy_pass("data_principal_rights") else "review" if policy_status("data_principal_rights") == "review" else "fail",
3640
+ "DPDP Act Sections 11, 12, 13, and 14",
3641
+ "high",
3642
+ "A policy should tell users how to access, correct, erase, complain, and nominate a representative.",
3643
+ "Add a rights request workflow with channel, expected response path, identity verification, and nomination language.",
3644
+ policy_evidence("data_principal_rights", "No access/correction/erasure/grievance/nomination workflow evidence found"),
3645
+ 10,
3646
+ ))
3647
+
3648
  vendor_status = "pass" if processor_count == 0 and tracker_count == 0 else "review"
3649
  checks.append(make_check(
3650
  "processor_inventory",
3651
  "Third-party processors and API destinations are inventoried",
3652
+ "pass" if vendor_status == "pass" and policy_pass("processor_vendor_sharing") else "review",
3653
  "DPDP Act Section 8 - Data Fiduciary accountability",
3654
  "medium" if vendor_status == "review" else "low",
3655
  "Every analytics, database, payment, support, AI, and auth provider needs a purpose, data category, contract owner, and retention note.",
 
3657
  [
3658
  f"Detected processors/providers: {', '.join(sorted({p.get('name') for p in detected_processors if p.get('name')}))}" if detected_processors else "No third-party processor signal detected",
3659
  f"API calls discovered: {len(api_calls)}" if api_calls else "",
3660
+ ] + policy_evidence("processor_vendor_sharing", "")[:1],
3661
  12,
3662
  "medium" if processor_count else "high",
3663
  ))
3664
 
3665
+ checks.append(make_check(
3666
+ "cross_border_transfer",
3667
+ "Cross-border transfer position is disclosed",
3668
+ "pass" if policy_pass("cross_border_transfer") else "review",
3669
+ "DPDP Act Section 16 and DPDP Rules 2025 transfer restrictions",
3670
+ "medium",
3671
+ "Users and auditors should know whether data leaves India and which safeguards/restrictions apply.",
3672
+ "Add a cross-border transfer statement, countries/regions where practical, and controls for restricted transfers.",
3673
+ policy_evidence("cross_border_transfer", "No cross-border transfer disclosure found; manual review needed"),
3674
+ 6,
3675
+ "low",
3676
+ ))
3677
+
3678
  checks.append(make_check(
3679
  "public_pii_exposure",
3680
  "No obvious personal data is exposed on public pages",
 
3692
  checks.append(make_check(
3693
  "secure_transport_headers",
3694
  "HTTPS and browser privacy/security headers are configured",
3695
+ "pass" if browser_security_ok and (policy_pass("security_safeguards") or policy_status("security_safeguards") == "review") else "review" if browser_security_ok else "fail",
3696
  "DPDP Act Section 8(5) - Reasonable security safeguards",
3697
  "medium",
3698
  "Transport security and browser headers reduce accidental disclosure and client-side abuse.",
 
3701
  f"HTTPS: {bool(compliance.get('https'))}",
3702
  f"HSTS: {(headers.get('strict-transport-security') or {}).get('note', 'missing')}",
3703
  f"Referrer-Policy: {(headers.get('referrer-policy') or {}).get('note', 'missing')}",
3704
+ ] + policy_evidence("security_safeguards", "")[:1],
3705
  8,
3706
  ))
3707
 
3708
+ checks.append(make_check(
3709
+ "breach_notification",
3710
+ "Personal data breach notification process is documented",
3711
+ "pass" if dpdp_pass("breach_notification") or policy_pass("breach_notification") else "fail",
3712
+ "DPDP Act security obligations and DPDP Rules 2025 breach notice",
3713
+ "high",
3714
+ "A real audit needs evidence that affected users and the Data Protection Board can be notified when a breach occurs.",
3715
+ "Add breach notification language, incident response owner, timelines, and Data Protection Board reporting path.",
3716
+ policy_evidence("breach_notification", "No personal data breach notification evidence found"),
3717
+ 9,
3718
+ ))
3719
+
3720
  child_status = "pass" if dpdp_pass("children_protection") else "review"
3721
  checks.append(make_check(
3722
  "children_data",
 
3772
  "manual_review": review[:4],
3773
  "detected_processors": detected_processors,
3774
  "evidence_register": evidence_register,
3775
+ "policy_control_matrix": policy_analysis,
3776
  "plain_english": [
3777
  f"We found {pii.get('pii_input_count', 0)} personal-data input signal(s).",
3778
  f"We found {tracker_count} tracker signal(s) and {processor_count} processor/provider signal(s).",
3779
  f"Public PII exposure count: {exposed.get('count', 0)}.",
3780
+ f"Policy evidence score: {policy_analysis.get('score', 'not available')}%.",
3781
  f"DPDP text-evidence checks passed: {dpdp.get('score', 'not available')} of {dpdp.get('total_checks', 'unknown')}.",
3782
  ],
3783
  "priority_actions": [
 
4589
  except Exception as e:
4590
  print(f"[!] Privacy policy page fetch failed: {e}")
4591
 
4592
+ # Combine visible homepage text + privacy page text for DPDP analysis.
4593
+ # Use clean text rather than raw HTML so evidence snippets are audit-readable.
4594
+ combined_compliance_text = visible_text.lower() + " " + privacy_page_text
4595
+ dpdp_policy_analysis = _analyze_dpdp_policy_text(combined_compliance_text, privacy_url_found)
4596
 
4597
  # ---- 15. DPDP ACT 2023 (India) COMPLIANCE CHECKS ----
4598
  # Based on the Digital Personal Data Protection Act, 2023
 
4890
  "https": is_https,
4891
  "privacy_policy": has_privacy_policy,
4892
  "privacy_policy_url": privacy_url_found,
4893
+ "policy_analysis": dpdp_policy_analysis,
4894
  "cookie_consent": has_cookie_consent,
4895
  "cmp_platforms": cmp_detected,
4896
  "terms_of_service": has_terms,