Not in the top 1,000 by GitHub stars
VenusOne-Lee/popia-compliance
Open-source POPIA privacy and security guardrails for South African builders, for every AI coding tool.
History
Review weekly changes to SKILL.md before updating. Dates mark the weeks in which we observed each version; checks apply to the recorded commits.
View weekly changes
SKILL.md
Week of Nov 2, 2026 · +0 / −0 lines
Before Network or broad shell → After Network or broad shell
1be23b4e77a1 → 1be23b4e77a1
Week of Oct 26, 2026 · Line counts unavailable
Before Not checked yet → After Network or broad shell
First observation → 1be23b4e77a1
No earlier version was observed for comparison.
adapters/lovable/skill/SKILL.md
Week of Nov 2, 2026 · +0 / −0 lines
Before Local scripts → After Local scripts
1be23b4e77a1 → 1be23b4e77a1
Week of Oct 26, 2026 · Line counts unavailable
Before Not checked yet → After Local scripts
First observation → 1be23b4e77a1
No earlier version was observed for comparison.
Verified commit 1be23b4e77a1d68014603ae6bc6e92dcf4232fe7
git clone --filter=blob:none --no-checkout https://github.com/VenusOne-Lee/popia-compliance.git .hotskills/venusone-lee-popia-compliance && git -C .hotskills/venusone-lee-popia-compliance checkout --detach 1be23b4e77a1d68014603ae6bc6e92dcf4232fe7
# Copy each selected skill directory from .hotskills/venusone-lee-popia-compliance into .agents/skills/<skill-name>/git clone --filter=blob:none --no-checkout https://github.com/VenusOne-Lee/popia-compliance.git .hotskills/venusone-lee-popia-compliance && git -C .hotskills/venusone-lee-popia-compliance checkout --detach 1be23b4e77a1d68014603ae6bc6e92dcf4232fe7
# Copy each selected skill directory from .hotskills/venusone-lee-popia-compliance into .cursor/skills/<skill-name>/Heat
1.7- Star growth, last 7 daysNot available
- Star growth, last 24 hoursNot available
- Rank movement, last 7 daysNot available
- Commits, last 30 daysNot available
- Accounts asking about it through MCP, last 7 daysNot available
- Hot votes this weekNot available
Heat never changes the order by stars, and sponsorship never touches it. How heat works
Stars over time
30-day change not yet available: not enough history.
Individual skills in this repository
- popia-compliance
/ - popia-kit
adapters/lovable/skill
How it works
popia-compliance
When your agent uses it
POPIA (South Africa's Protection of Personal Information Act) compliance and security audit skill for builders. Use whenever building, reviewing, auditing or shipping any website, web app, mobile app, API, CRM setup, AI feature or automation that touches personal information of South Africans - sign-up forms, lead capture, contact forms, checkout, user accounts, SA ID numbers, analytics/cookies, email or WhatsApp marketing, AI/LLM features, Supabase/Lovable/Firebase/Next.js/WordPress/Salesforce builds - and whenever the user mentions POPIA, POPI, PAIA, Information Regulator, Information Officer, data protection, privacy policy, consent, data breach / security compromise, DSAR, data retention, or "is this compliant". Trigger even if POPIA isn't named but the product serves South African users and collects personal data.
What it loads into context
Frontmatter
182 tokensAlways available to the agent
SKILL.md
3,288 tokensWhen the skill is activated
Reference documents
115,365 tokensLoaded on request
Token counts are estimates. Scripts run separately; they are not counted as reference context.
The playbook
- Pick the mode
- Core mental model (keep this loaded)
- Build mode
- Audit mode
- Non-negotiables (stop and flag, don't silently "fix")
- Output conventions
- File index
- Using the components
- Disclaimer
Scripts executed separately
bin/popia-kit.jsevals/fixtures/chatbot/support_bot.pyevals/fixtures/env-service-role/supabaseClient.tsevals/fixtures/loan-decision/decide.tsevals/fixtures/newsletter-form/Newsletter.tsxevals/fixtures/react-preticked/SignupForm.tsxevals/fixtures/repo-audit/src/SignupForm.tsxevals/fixtures/repo-audit/src/logger.tsevals/run_evals.pyhosted/site-check/adapters/node-pinned-fetch.tshosted/site-check/adapters/node-supabase-rate-limit.tshosted/site-check/adapters/package.jsonhosted/site-check/adapters/test/node.smoke.tshosted/site-check/supabase/functions/site-check/core.tshosted/site-check/supabase/functions/site-check/index.tshosted/site-check/tests/actions.test.tshosted/site-check/tests/checks.test.tshosted/site-check/tests/guard.test.tshosted/site-check/tests/handler.test.tshosted/site-check/tests/hardening.test.tshosted/site-check/tests/helpers.tshosted/site-check/tests/sql/run_sql_tests.shmcp/__init__.pymcp/server.pymcp/smoke_test.pypackage.jsonregistry/build.pyregistry/popia/account/privacy-centre.tsxregistry/popia/consent/consent-banner.tsxregistry/popia/consent/consent-gate.tsxregistry/popia/consent/consent-provider.tsxregistry/popia/forms/collection-notice.tsxregistry/popia/forms/marketing-opt-in.tsxregistry/popia/forms/sa-id-input.tsxregistry/popia/lib/consent-record.tsregistry/popia/lib/redact.tsregistry/popia/lib/sa-id.tsregistry/popia/supabase/functions/_shared/privacy-config.tsregistry/popia/supabase/functions/consent/index.tsregistry/popia/supabase/functions/dsar-delete/index.tsregistry/popia/supabase/functions/dsar-export/index.tsregistry/popia/supabase/functions/unsubscribe/index.tsscripts/__init__.pyscripts/build_adapters.pyscripts/cli.pyscripts/popia_badge.pyscripts/popia_fleet.pyscripts/popia_init.pyscripts/popia_operators.pyscripts/popia_pack.pyscripts/popia_scan.pyscripts/popia_vendors.pyscripts/regwatch.pyscripts/rules/__init__.pyscripts/rules/access.pyscripts/rules/consent.pyscripts/rules/pii.pyscripts/rules/repo.pyscripts/rules/secrets.pyscripts/rules/thirdparty.pyscripts/rules/web.pyscripts/sa_id.pyscripts/site_check.pyscripts/style_lint.pyscripts/supabase_check.pytests/__init__.pytests/automation/__init__.pytests/automation/fixtures/__init__.pytests/automation/test_badge.pytests/automation/test_cli.pytests/automation/test_fleet.pytests/automation/test_init.pytests/automation/test_mcp.pytests/automation/test_regwatch.pytests/fixtures/demo-app/package.jsontests/fixtures/demo-app/src/ai.tstests/fixtures/demo-app/src/components/SignupForm.tsxtests/fixtures/demo-app/src/newsletter.tstests/fixtures/demo-app/supabase/functions/lead/index.tstests/fixtures/operators-app/api/triage.pytests/fixtures/operators-app/package.jsontests/fixtures/operators-app/src/checkout.tstests/fixtures/operators-app/src/sms.jstests/fixtures/operators-app/supabase-edge.tstests/fixtures/operators-app/tests/checkout.test.tstests/pack/__init__.pytests/pack/test_pack.pytests/scanner/__init__.pytests/scanner/test_baseline.pytests/scanner/test_operators.pytests/scanner/test_rules.pytests/scanner/test_site_check.pytests/security/__init__.pytests/security/test_hardening.pytests/supabase/__init__.pytests/supabase/run_live.shtests/supabase/test_supabase_check.py
Files
.github/CODEOWNERSasset271 bytes.github/ISSUE_TEMPLATE/bug.ymlasset389 bytes.github/ISSUE_TEMPLATE/config.ymlasset374 bytes.github/ISSUE_TEMPLATE/false-positive.ymlasset1,384 bytes.github/ISSUE_TEMPLATE/law-change.ymlasset1,706 bytes.github/ISSUE_TEMPLATE/legal-reviewer.ymlasset2,312 bytes.github/ISSUE_TEMPLATE/new-rule.ymlasset729 bytes.github/ISSUE_TEMPLATE/translation.ymlasset446 bytes.github/PULL_REQUEST_TEMPLATE.mddoc848 bytes.github/dependabot.ymlasset272 bytes.github/workflows/ci.ymlasset7,311 bytes.github/workflows/regwatch.ymlasset1,972 bytes.github/workflows/release.ymlasset5,197 bytes.github/workflows/security.ymlasset2,682 bytes.gitignoreasset133 bytes.popiaignoreasset140 bytes.pre-commit-hooks.yamlasset491 bytes.semgrepignoreasset46 bytesCHANGELOG.mddoc13,480 bytesCODE_OF_CONDUCT.mddoc693 bytesCONTRIBUTING.mddoc6,704 bytesDISCLAIMER.mddoc2,492 bytesGOVERNANCE.mddoc5,127 bytesLICENSEasset1,116 bytesLICENSE-CONTENTasset991 bytesLICENSING.mddoc827 bytesREADME.mddoc10,618 bytesSECURITY.mddoc2,146 bytesSKILL.mdcore13,828 bytesSKILL.mdcore13,828 bytesSKILL.mdcore13,828 bytesSKILL.mdcore13,828 bytesTRADEMARKS.mddoc729 bytesaction.ymlasset7,621 bytesadapters/README.mddoc1,280 bytesadapters/_core.mddoc5,223 bytesadapters/agents/AGENTS.mddoc5,325 bytesadapters/chatgpt/custom-gpt-instructions.mddoc5,437 bytesadapters/claude/CLAUDE.mddoc5,433 bytesadapters/copilot/.github/copilot-instructions.mddoc5,325 bytesadapters/cursor/.cursor/rules/popia.mdcasset5,470 bytesadapters/gemini/GEMINI.mddoc5,325 bytesadapters/lovable/knowledge.mddoc5,619 bytesadapters/windsurf/.windsurf/rules/popia.mddoc5,403 bytesadapters/windsurf/.windsurfrulesasset5,325 bytesbin/popia-kit.jsruns1,105 bytesdocs/automation.mddoc12,593 bytesdocs/mcp.mddoc10,093 bytesdocs/quickstart.mddoc8,096 bytesdocs/releasing.mddoc3,385 bytesdocs/site-check-bot.mddoc2,893 bytesevals/README.mddoc11,908 bytesevals/cases/audit-repo.jsonasset1,190 bytesevals/cases/breach-exposed-service-role-key.jsonasset1,263 bytesevals/cases/breach-misdirected-email.jsonasset971 bytesevals/cases/breach-rls-off-week.jsonasset993 bytesevals/cases/build-ai-chatbot-customer-data.jsonasset961 bytesevals/cases/build-airbnb-guest-ids.jsonasset1,458 bytesevals/cases/build-analytics-pixels.jsonasset860 bytesevals/cases/build-automated-loan-decision.jsonasset859 bytesevals/cases/build-delete-account.jsonasset851 bytesevals/cases/build-face-verification.jsonasset926 bytesevals/cases/build-fine-tune-customer-data.jsonasset868 bytesevals/cases/build-kids-app.jsonasset1,064 bytesevals/cases/build-newsletter-cursor.jsonasset1,090 bytesevals/cases/build-newsletter-lovable.jsonasset1,027 bytesevals/cases/build-newsletter-signup.jsonasset1,137 bytesevals/cases/build-sa-id-without-need.jsonasset922 bytesevals/cases/build-supabase-table.jsonasset930 bytesevals/cases/build-whatsapp-broadcast.jsonasset876 bytesevals/cases/docs-access-request.jsonasset824 bytesevals/cases/docs-cookie-banner.jsonasset748 bytesevals/cases/docs-objection.jsonasset819 bytesevals/cases/docs-privacy-policy.jsonasset1,112 bytesevals/cases/explain-cold-b2b-email.jsonasset879 bytesevals/cases/explain-hr-criminal-checks.jsonasset915 bytesevals/cases/explain-popia-fines.jsonasset869 bytesevals/cases/explain-uncertain-answer.jsonasset873 bytesevals/cases/explain-us-cloud-hosting.jsonasset896 bytesevals/cases/refuse-backdate-consent.jsonasset916 bytesevals/cases/refuse-buy-lead-list.jsonasset1,054 bytesevals/cases/refuse-preticked-checkbox.jsonasset980 bytesevals/fixtures/chatbot/support_bot.pyruns776 bytesevals/fixtures/env-service-role/env.local.txtdoc296 bytesevals/fixtures/env-service-role/supabaseClient.tsruns203 bytesevals/fixtures/guest-checkin/guests.csvasset384 bytesevals/fixtures/guest-checkin/whatsapp-template.txtdoc269 bytesevals/fixtures/kids-app/analytics.htmlasset452 bytesevals/fixtures/kids-app/schema.sqlasset255 bytesevals/fixtures/loan-decision/decide.tsruns357 bytesevals/fixtures/misdirected-email/incident.mddoc330 bytesevals/fixtures/newsletter-form/Newsletter.tsxruns305 bytesevals/fixtures/react-preticked/SignupForm.tsxruns661 bytesevals/fixtures/repo-audit/README.mddoc87 bytesevals/fixtures/repo-audit/src/SignupForm.tsxruns661 bytesevals/fixtures/repo-audit/src/index.htmlasset452 bytesevals/fixtures/repo-audit/src/logger.tsruns173 bytesevals/fixtures/repo-audit/supabase/migrations/20261001_create_leads.sqlasset397 bytesevals/fixtures/supabase-no-rls/20261001_create_leads.sqlasset397 bytesevals/published/2026-10-18-claude-opus-5-5-run1.jsonasset353,163 bytesevals/published/2026-10-18-claude-opus-5-5-run1.mddoc2,225 bytesevals/published/2026-10-18-claude-opus-5-5-run2-fixes.jsonasset44,783 bytesevals/published/2026-10-18-claude-opus-5-5-run2-fixes.mddoc598 bytesevals/run_evals.pyruns28,064 bytesexamples/github-workflow-popia-scan.ymlasset992 bytesexamples/github-workflow-site-check.ymlasset918 byteshosted/site-check/.gitignoreasset14 byteshosted/site-check/README.mddoc19,062 byteshosted/site-check/adapters/deno.jsonasset345 byteshosted/site-check/adapters/node-pinned-fetch.tsruns3,196 byteshosted/site-check/adapters/node-supabase-rate-limit.tsruns1,050 byteshosted/site-check/adapters/package-lock.jsonasset5,516 byteshosted/site-check/adapters/package.jsonruns497 byteshosted/site-check/adapters/test/node.smoke.tsruns8,309 byteshosted/site-check/deno.jsonasset894 byteshosted/site-check/deno.lockasset2,021 byteshosted/site-check/report/actions.jsonasset9,037 byteshosted/site-check/supabase/functions/site-check/core.tsruns57,861 byteshosted/site-check/supabase/functions/site-check/index.tsruns934 byteshosted/site-check/supabase/migrations/site_check_rate.sqlasset2,529 byteshosted/site-check/tests/actions.test.tsruns2,342 byteshosted/site-check/tests/checks.test.tsruns20,344 byteshosted/site-check/tests/guard.test.tsruns20,581 byteshosted/site-check/tests/handler.test.tsruns18,395 byteshosted/site-check/tests/hardening.test.tsruns19,860 byteshosted/site-check/tests/helpers.tsruns4,037 byteshosted/site-check/tests/sql/run_sql_tests.shruns5,871 bytesi18n/README.mddoc4,726 bytesi18n/af/breach-notice-short.mddoc1,121 bytesi18n/af/collection-notice.mddoc1,165 bytesi18n/af/cookie-banner.jsonasset1,226 bytesi18n/af/marketing-consent.mddoc784 bytesi18n/en/breach-notice-short.mddoc688 bytesi18n/en/collection-notice.mddoc677 bytesi18n/en/cookie-banner.jsonasset743 bytesi18n/en/marketing-consent.mddoc374 bytesi18n/tn/breach-notice-short.mddoc1,159 bytesi18n/tn/collection-notice.mddoc1,294 bytesi18n/tn/cookie-banner.jsonasset1,305 bytesi18n/tn/marketing-consent.mddoc835 bytesi18n/xh/breach-notice-short.mddoc1,088 bytesi18n/xh/collection-notice.mddoc1,195 bytesi18n/xh/cookie-banner.jsonasset1,252 bytesi18n/xh/marketing-consent.mddoc848 bytesi18n/zu/breach-notice-short.mddoc1,093 bytesi18n/zu/collection-notice.mddoc1,180 bytesi18n/zu/cookie-banner.jsonasset1,252 bytesi18n/zu/marketing-consent.mddoc843 bytesmcp/README.mddoc1,061 bytesmcp/__init__.pyruns0 bytesmcp/server.pyruns32,264 bytesmcp/smoke_test.pyruns6,719 bytesmcp/tools.jsonasset7,045 bytespackage.jsonruns1,116 bytespacks/README.mddoc6,011 bytespacks/_template.mddoc1,643 bytespacks/ecommerce.mddoc14,611 bytespacks/education-and-children.mddoc14,597 bytespacks/fintech.mddoc22,274 bytespacks/health.mddoc17,173 bytespacks/hr-and-recruitment.mddoc16,922 bytespacks/short-term-rentals.mddoc16,918 bytespyproject.tomlasset3,347 bytesreferences/01-popia-legal-map.mddoc24,438 bytesreferences/02-build-patterns.mddoc17,948 bytesreferences/03-security-controls.mddoc11,051 bytesreferences/04-stack-playbooks.mddoc11,362 bytesreferences/05-direct-marketing.mddoc5,303 bytesreferences/06-cross-border-and-operators.mddoc4,352 bytesreferences/07-breach-response.mddoc5,751 bytesreferences/08-ai-and-automated-decisions.mddoc4,807 bytesreferences/09-audit-procedure.mddoc11,000 bytesreferences/10-regulatory-watch.mddoc5,750 bytesregistry/README.mddoc18,661 bytesregistry/build.pyruns4,168 bytesregistry/popia/account/privacy-centre.tsxruns17,061 bytesregistry/popia/consent/consent-banner.tsxruns8,730 bytesregistry/popia/consent/consent-gate.tsxruns9,023 bytesregistry/popia/consent/consent-provider.tsxruns8,266 bytesregistry/popia/forms/collection-notice.tsxruns3,253 bytesregistry/popia/forms/marketing-opt-in.tsxruns7,869 bytesregistry/popia/forms/sa-id-input.tsxruns4,170 bytesregistry/popia/lib/consent-record.tsruns4,598 bytesregistry/popia/lib/redact.tsruns6,833 bytesregistry/popia/lib/sa-id.tsruns3,501 bytesregistry/popia/supabase/functions/_shared/privacy-config.tsruns14,640 bytesregistry/popia/supabase/functions/consent/index.tsruns4,841 bytesregistry/popia/supabase/functions/dsar-delete/index.tsruns5,521 bytesregistry/popia/supabase/functions/dsar-export/index.tsruns3,945 bytesregistry/popia/supabase/functions/unsubscribe/index.tsruns5,418 bytesregistry/popia/supabase/migrations/0001_privacy_schema.sqlasset14,123 bytesregistry/popia/supabase/migrations/0002_retention.sqlasset3,859 bytesregistry/popia/supabase/migrations/0003_security_checks.sqlasset3,444 bytesregistry/popia/supabase/migrations/0004_function_guards.sqlasset6,381 bytesregistry/r/collection-notice.jsonasset3,898 bytesregistry/r/consent-banner.jsonasset9,855 bytesregistry/r/consent-gate.jsonasset10,025 bytesregistry/r/consent-provider.jsonasset9,213 bytesregistry/r/consent-record.jsonasset5,300 bytesregistry/r/consent-system.jsonasset32,913 bytesregistry/r/marketing-opt-in.jsonasset8,934 bytesregistry/r/privacy-centre.jsonasset18,596 bytesregistry/r/redact.jsonasset7,772 bytesregistry/r/registry.jsonasset10,250 bytesregistry/r/sa-id-input.jsonasset5,029 bytesregistry/r/sa-id.jsonasset4,114 bytesregistry/r/supabase-privacy-functions.jsonasset37,294 bytesregistry/r/supabase-privacy-schema.jsonasset29,734 bytesregistry/registry.jsonasset10,250 bytesregwatch/sources.jsonasset1,154 bytesregwatch/state.jsonasset16,254 bytesscripts/__init__.pyruns0 bytesscripts/build_adapters.pyruns6,890 bytesscripts/cli.pyruns4,217 bytesscripts/popia_badge.pyruns5,218 bytesscripts/popia_fleet.pyruns15,458 bytesscripts/popia_init.pyruns7,441 bytesscripts/popia_operators.pyruns33,731 bytesscripts/popia_pack.pyruns44,438 bytesscripts/popia_scan.pyruns25,995 bytesscripts/popia_vendors.pyruns9,723 bytesscripts/regwatch.pyruns13,722 bytesscripts/rules/__init__.pyruns0 bytesscripts/rules/access.pyruns6,799 bytesscripts/rules/consent.pyruns10,773 bytesscripts/rules/pii.pyruns7,164 bytesscripts/rules/repo.pyruns7,157 bytesscripts/rules/secrets.pyruns5,595 bytesscripts/rules/thirdparty.pyruns5,217 bytesscripts/rules/web.pyruns11,818 bytesscripts/sa_id.pyruns4,042 bytesscripts/site_check.pyruns26,645 bytesscripts/sql/supabase_inventory.sqlasset5,533 bytesscripts/style_lint.pyruns3,707 bytesscripts/supabase_check.pyruns18,544 bytesserver.jsonasset812 bytestemplates/audit-report.mddoc8,528 bytestemplates/breach-notice-data-subject.mddoc5,453 bytestemplates/breach-regulator-checklist.mddoc3,412 bytestemplates/consent-patterns.mddoc7,704 bytestemplates/cookie-notice.mddoc2,795 bytestemplates/dsar-runbook.mddoc6,343 bytestemplates/operator-agreement-checklist.mddoc5,314 bytestemplates/pack-questionnaire.jsonasset18,548 bytestemplates/paia-manual-scaffold.mddoc5,380 bytestemplates/pia-template.mddoc6,692 bytestemplates/privacy-notice.mddoc10,456 bytestemplates/retention-schedule.csvasset4,033 bytestemplates/ropa.csvasset1,484 bytestests/__init__.pyruns0 bytestests/automation/__init__.pyruns0 bytestests/automation/fixtures/__init__.pyruns0 bytestests/automation/fixtures/regulator-list.htmlasset1,419 bytestests/automation/test_badge.pyruns3,031 bytestests/automation/test_cli.pyruns2,793 bytestests/automation/test_fleet.pyruns4,501 bytestests/automation/test_init.pyruns4,444 bytestests/automation/test_mcp.pyruns4,104 bytestests/automation/test_regwatch.pyruns4,018 bytestests/fixtures/README.mddoc379 bytestests/fixtures/demo-app/firestore.rulesasset162 bytestests/fixtures/demo-app/index.htmlasset315 bytestests/fixtures/demo-app/package.jsonruns180 bytestests/fixtures/demo-app/src/ai.tsruns542 bytestests/fixtures/demo-app/src/components/SignupForm.tsxruns956 bytestests/fixtures/demo-app/src/newsletter.tsruns288 bytestests/fixtures/demo-app/supabase/functions/lead/index.tsruns244 bytestests/fixtures/demo-app/supabase/migrations/20260101000000_init.sqlasset498 bytestests/fixtures/operators-app/.env.exampleasset49 bytestests/fixtures/operators-app/api/triage.pyruns386 bytestests/fixtures/operators-app/package.jsonruns242 bytestests/fixtures/operators-app/public/contact.htmlasset243 bytestests/fixtures/operators-app/requirements.txtdoc91 bytestests/fixtures/operators-app/src/checkout.tsruns594 bytestests/fixtures/operators-app/src/sms.jsruns710 bytestests/fixtures/operators-app/supabase-edge.tsruns292 bytestests/fixtures/operators-app/tests/checkout.test.tsruns68 bytestests/fixtures/supabase/lovable-hardened.jsonasset4,401 bytestests/fixtures/supabase/seed.sqlasset3,622 bytestests/fixtures/supabase/seeded-risky.jsonasset3,739 bytestests/pack/__init__.pyruns0 bytestests/pack/kasi-bookings-answers.jsonasset5,297 bytestests/pack/test_pack.pyruns12,583 bytestests/scanner/__init__.pyruns0 bytestests/scanner/test_baseline.pyruns5,799 bytestests/scanner/test_operators.pyruns7,813 bytestests/scanner/test_rules.pyruns9,843 bytestests/scanner/test_site_check.pyruns5,305 bytestests/security/__init__.pyruns0 bytestests/security/test_hardening.pyruns7,177 bytestests/supabase/__init__.pyruns0 bytestests/supabase/run_live.shruns1,206 bytestests/supabase/test_supabase_check.pyruns10,345 bytes
Read SKILL.md
# POPIA Compliance & Security Audit
You are acting as a privacy-engineering partner for a South African builder. Your job is to make the product **lawful by design** under POPIA (Act 4 of 2013) and its Regulations (as amended 17 April 2025), and to **prove it** with evidence: scanner output, a data map, and an audit report a lawyer or Information Officer can sign off.
Law in this skill is current to **18 October 2026**. Anything time-sensitive is collected in `references/10-regulatory-watch.md` with dates - check it, and re-verify anything flagged before relying on it.
> This skill gives engineering and compliance guidance, not legal advice. Say so once per deliverable (not on every message). Recommend a POPIA attorney for: special personal information at scale, children's data, prior authorisation (s57), cross-border transfers of special/children's info, credit/insurance/health/financial-services products, and anything already in front of the Regulator.
## Pick the mode
Read the request and pick one (or chain them):
| Mode | When | Go to |
|---|---|---|View the complete source on GitHubpopia-kit
When your agent uses it
POPIA privacy and security guardrails for South African apps, plus a read-only database check. Use when building or changing anything that handles personal information, before publishing, or when asked for a POPIA, privacy or security review.
What it loads into context
Frontmatter
54 tokensAlways available to the agent
SKILL.md
3,752 tokensWhen the skill is activated
Reference documents
0 tokensLoaded on request
Token counts are estimates. Scripts run separately; they are not counted as reference context.
The playbook
- Always apply when writing code
- Stop and ask the human before
- Refuse
- When asked to audit
- Style
- Lovable specifics
- Database check (read-only)
Scripts executed separately
../../../scripts/popia_operators.py../../../scripts/popia_operators.py../../../scripts/popia_scan.py
Files
SKILL.mdcore15,494 bytes../../../scripts/popia_operators.pyruns33,731 bytes../../../scripts/popia_operators.pyruns33,731 bytes../../../scripts/popia_scan.pyruns25,995 bytes
Read SKILL.md
<!-- Generated from adapters/_core.md by scripts/build_adapters.py. Edit the core, not this file. -->
# POPIA privacy and security guardrails
These rules apply whenever you build, change or review anything that touches personal information of people in South Africa: forms, sign-ups, accounts, CRM, analytics, marketing, AI features, uploads, payments. They come from POPIA (Protection of Personal Information Act 4 of 2013) and its Regulations as amended 17 April 2025. Engineering guidance, not legal advice.
Full skill, scanner and references: https://github.com/VenusOne-Lee/popia-compliance
## Always apply when writing code
1. **Minimise (s10).** Every field needs a purpose. Don't collect SA ID numbers unless a law (FICA, tax, payroll, credit) or the core service requires it. Prefer date of birth or an age check.
2. **Lock down access (s19).** Enable row-level security on every Supabase/Postgres table holding personal information, with owner-scoped policies. Never `using (true)` for select/update/delete. Firebase rules compare `request.auth.uid` with the owner. Storage buckets with uploads are private; serve via short-lived signed URLs.
3. **Keep secrets server-side (s19).** No `service_role`, admin or secret keys in client code or in `VITE_`, `NEXT_PUBLIC_`, `EXPO_PUBLIC_`, `REACT_APP_` variables. Derive user identity from the verified session or JWT, never from the request body.
4. **Encrypt (s19).** HTTPS everywhere with HSTS. Field-level encryption or tokenisation for ID numbers, bank details, health data and biometrics. Passwords with argon2id, bcrypt or scrypt only.View the complete source on GitHubSkills included
- popia-compliance
- popia-kit
Repository facts
- License
- MIT
- Created
- Oct 18, 2026
- Last pushed
- Oct 27, 2026
- Forks
- 2
- Open issues
- 7
- Project activity
- Active
Before you install
Network or broad shellAutomated signals, not a guarantee
Checked Nov 7, 2026
Not fully analyzed: some files were too long, unreadable or missing. This never lowers the level.
Show 160 notes
SKILL.md:1Analysis incomplete: Referenced executable target unavailable; not analyzedSKILL.md:1Analysis incomplete: Referenced executable target unavailable; not analyzedSKILL.md:1Analysis incomplete: Referenced executable target unavailable; not analyzedevals/published/2026-10-18-claude-opus-5-5-run1.json:21Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:19Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:20Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:91[Credential-related source omitted]evals/published/2026-10-18-claude-opus-5-5-run1.json:89Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:90Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:155Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:153Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:154Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:210Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:208Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:209Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:267[Credential-related source omitted]evals/published/2026-10-18-claude-opus-5-5-run1.json:265Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:266Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:323Analysis incomplete: physical or joined line exceeds character limitevals/published/2026-10-18-claude-opus-5-5-run1.json:321Analysis incomplete: physical or joined line exceeds character limit
- No broad tool permissions
- 5 dangerous shell commandsin 4 files: .github/workflows/ci.yml:160 .github/workflows/release.yml:79 hosted/site-check/tests/sql/run_sql_tests.sh:14 and 1 more
- 1,078 network referencesin 100 files: hosted/site-check/tests/handler.test.ts:14 hosted/site-check/tests/guard.test.ts:227 regwatch/state.json:6 and 97 more
- 1 obfuscation signalin 1 file: registry/popia/supabase/functions/_shared/privacy-config.ts:295
- No instruction overrides
- 100 executable scriptsin 97 files: scripts/popia_operators.py:1 scripts/popia_scan.py:1 bin/popia-kit.js:1 and 94 more
- 92 references to secretsin 37 files: tests/supabase/test_supabase_check.py:156 evals/run_evals.py:17 mcp/server.py:257 and 34 more
- No requests to vote in Hot
Show all 1,276 signals by file
.github/ISSUE_TEMPLATE/config.yml
- Network accessline 2
contact_links: - name: Security vulnerability in this project url: https://github.com/VenusOne-Lee/popia-compliance/security/advisories/new - Network accessline 3
- name: Security vulnerability in this project url: https://github.com/VenusOne-Lee/popia-compliance/security/advisories/new about: Please report vulnerabilities privately. - Network accessline 4
url: https://github.com/VenusOne-Lee/popia-compliance/security/advisories/new
.github/ISSUE_TEMPLATE/legal-reviewer.yml
- Network accessline 13
Please read [the reviewer terms in GOVERNANCE.md](https://github.com/VenusOne-Lee/popia-compliance/blob/main/GOVERNANCE.md#reviewer-terms) first. In short: your review is not legal advice to users and creates no client relationship.
.github/workflows/ci.yml
.github/workflows/release.yml
- Network accessline 67
environment: name: pypi url: https://pypi.org/project/popia-kit/ - Network accessline 68
name: pypi url: https://pypi.org/project/popia-kit/ permissions: - Network accessline 69
url: https://pypi.org/project/popia-kit/ - Dangerous shell operationline 79
run: rm -f dist/*.tgz - Network accessline 89
environment: name: npm url: https://www.npmjs.com/package/popia-kit - Network accessline 90
name: npm url: https://www.npmjs.com/package/popia-kit permissions: - Network accessline 91
url: https://www.npmjs.com/package/popia-kit - Network accessline 101
with: node-version: "22" registry-url: "https://registry.npmjs.org" - Network accessline 103
registry-url: "https://registry.npmjs.org" - Access to secretsline 110
[Credential-related source omitted]
.gitignore
- Access to secretsline 5
[Credential-related source omitted]
CHANGELOG.md
- Network accessline 44
### Added - Release workflow: a version tag publishes to PyPI and npm with trusted publishing and npm provenance, then creates a GitHub release. Setup in docs/releasing.md. - One-line install, no clone needed: `uvx --from git+https://github - Network accessline 45
- Release workflow: a version tag publishes to PyPI and npm with trusted publishing and npm provenance, then creates a GitHub release. Setup in docs/releasing.md. - One-line install, no clone needed: `uvx --from git+https://github.com/Venus - Network accessline 46
- One-line install, no clone needed: `uvx --from git+https://github.com/VenusOne-Lee/popia-compliance popia-kit scan .`, the `pipx run` equivalent, or `npx -y github:VenusOne-Lee/popia-compliance scan .`. One `popia-kit` command with `scan` - Network accessline 73
- MCP server: unexpected errors return and log the exception type only; `popia_scan` refuses filesystem roots, the home folder and credential or system folders (optionally limited further with `POPIA_SCAN_ROOTS`); `popia_generate_pack` writ - Network accessline 74
- Scanner: no longer follows symlinks out of the scanned tree. - Fleet scan: clones only `https://` or `git@host:` URLs, separates the URL with `--`, disables hooks, submodules, LFS smudge and the file and ext transports, and redacts tokens - Network accessline 75
- Fleet scan: clones only `https://` or `git@host:` URLs, separates the URL with `--`, disables hooks, submodules, LFS smudge and the file and ext transports, and redacts tokens from clone errors.
CODE_OF_CONDUCT.md
- Network accessline 3
This project adopts the [Contributor Covenant, version 2.1](https://www.contributor-covenant.org/version/2/1/code_of_conduct/).
GOVERNANCE.md
- Network accessline 23
**To volunteer:** email **popiakit@venusone.africa**, or, if you're on GitHub, open a [Legal reviewer: volunteer](https://github.com/VenusOne-Lee/popia-compliance/issues/new?template=legal-reviewer.yml) issue (that issue is public).
LICENSE-CONTENT
- Network accessline 13
Suggested attribution: "Based on the POPIA Compliance Skill by Venus One (Venus ARC (Pty) Ltd) and contributors, https://github.com/VenusOne-Lee/popia-compliance, licensed under CC BY 4.0." - Network accessline 15
https://github.com/VenusOne-Lee/popia-compliance, licensed under CC BY 4.0." - Network accessline 17
Full legal code: https://creativecommons.org/licenses/by/4.0/legalcode
README.md
- Network accessline 3
[](https://pypi.org/project/popia-kit/) [](https://github.com/VenusOne-Lee/popia-complianc - Network accessline 41
**No install:** check a live site you own in your browser at [popiakit.co.za/check](https://popiakit.co.za/check). It runs the same passive site check as `popia-kit site-check`, and you can download the report as a PDF. Project site: [popia - Network accessline 61
Python 3.8 or newer and nothing else. No install needed: run it from [PyPI](https://pypi.org/project/popia-kit/) with whichever tool you already have. - Network accessline 74
To keep it installed: `pipx install popia-kit`, then use `popia-kit` anywhere. For the latest code on `main`, use `uvx --from git+https://github.com/VenusOne-Lee/popia-compliance popia-kit scan .` - Network accessline 81
| `popia-kit scan . --changed-since origin/main` | Report only findings in files changed since a branch | | `popia-kit operators ./my-app --format csv --out operators.csv` | Every third-party API or SDK the code calls, and the personal info - Network accessline 82
| `popia-kit operators ./my-app --format csv --out operators.csv` | Every third-party API or SDK the code calls, and the personal information that appears to reach each. Your operator and cross-border list | | `popia-kit site-check https:// - Network accessline 83
| `popia-kit site-check https://example.co.za --out site-check.md` | Passive check of a live site you own | - Network accessline 93
git clone https://github.com/VenusOne-Lee/popia-compliance - Network accessline 135
- **Report a law change:** open a [law-change issue](.github/ISSUE_TEMPLATE/law-change.yml). A monthly automated watch of the Regulator's site opens these too. - **Report a false positive:** open a [false positive issue](.github/ISSUE_TEMPL - Network accessline 136
- **Report a false positive:** open a [false positive issue](.github/ISSUE_TEMPLATE/false-positive.yml) with the rule ID and a small example. - **First time here:** look for issues labelled [good first issue](https://github.com/VenusOne-Lee - Network accessline 137
- **First time here:** look for issues labelled [good first issue](https://github.com/VenusOne-Lee/popia-compliance/labels/good%20first%20issue).
SKILL.md
- Network accessline 81
python3 scripts/site_check.py https://example.co.za --out site-check.md
action.yml
adapters/_core.md
- Network accessline 5
Full skill, scanner and references: https://github.com/VenusOne-Lee/popia-compliance
adapters/agents/AGENTS.md
- Network accessline 6
Full skill, scanner and references: https://github.com/VenusOne-Lee/popia-compliance
adapters/chatgpt/custom-gpt-instructions.md
- Network accessline 6
Full skill, scanner and references: https://github.com/VenusOne-Lee/popia-compliance
adapters/claude/CLAUDE.md
- Network accessline 6
Full skill, scanner and references: https://github.com/VenusOne-Lee/popia-compliance
adapters/copilot/.github/copilot-instructions.md
- Network accessline 6
Full skill, scanner and references: https://github.com/VenusOne-Lee/popia-compliance
adapters/cursor/.cursor/rules/popia.mdc
- Network accessline 11
Full skill, scanner and references: https://github.com/VenusOne-Lee/popia-compliance
adapters/gemini/GEMINI.md
- Network accessline 6
Full skill, scanner and references: https://github.com/VenusOne-Lee/popia-compliance
adapters/lovable/knowledge.md
- Network accessline 6
Full skill, scanner and references: https://github.com/VenusOne-Lee/popia-compliance
adapters/lovable/skill/SKILL.md
- Network accessline 12
Full skill, scanner and references: https://github.com/VenusOne-Lee/popia-compliance - Network accessline 170
The same query and rules ship as `uvx popia-kit supabase sql` and `uvx popia-kit supabase check result.json`. For the live site, use the free passive check at https://popiakit.co.za/check.
adapters/windsurf/.windsurf/rules/popia.md
- Network accessline 10
Full skill, scanner and references: https://github.com/VenusOne-Lee/popia-compliance
adapters/windsurf/.windsurfrules
- Network accessline 6
Full skill, scanner and references: https://github.com/VenusOne-Lee/popia-compliance
bin/popia-kit.js
- Executable scriptline 1
#!/usr/bin/env node - Network accessline 23
process.exit(r.status === null ? 1 : r.status); } console.error('popia-kit needs Python 3.8 or newer. Install it from https://www.python.org/downloads/ and try again.'); - Network accessline 24
} console.error('popia-kit needs Python 3.8 or newer. Install it from https://www.python.org/downloads/ and try again.'); process.exit(1); - Network accessline 25
console.error('popia-kit needs Python 3.8 or newer. Install it from https://www.python.org/downloads/ and try again.');
docs/automation.md
- Network accessline 99
repos: - repo: https://github.com/VenusOne-Lee/popia-compliance - Network accessline 100
- repo: https://github.com/VenusOne-Lee/popia-compliance - Network accessline 204
https://github.com/your-org/lovable-project-one.git - Network accessline 205
https://github.com/your-org/lovable-project-two.git - Network accessline 228
`--shields-json` writes a [shields.io endpoint](https://shields.io/badges/endpoint-badge) file. Commit it (or publish it from CI to a branch or Pages) and use: - Network accessline 231

docs/mcp.md
- Access to secretsline 149
[Credential-related source omitted]
docs/quickstart.md
- Network accessline 15
Only want to check a live site? Use [popiakit.co.za/check](https://popiakit.co.za/check) in your browser. Nothing to install. - Network accessline 26
R=https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r - Network accessline 95
uvx popia-kit site-check https://your-site.co.za --out site-check.md
docs/releasing.md
- Network accessline 53
`server.json` describes the MCP server for the [official MCP Registry](https://registry.modelcontextprotocol.io), which Claude, Cursor, VS Code and other clients use to find servers. The registry checks that the PyPI package is ours by find - Network accessline 58
2. From the repository root on `main`: `mcp-publisher login github`. Sign in as VenusOne-Lee in the browser. 3. `mcp-publisher publish`. It reads `server.json`. 4. Check it is listed: `curl "https://registry.modelcontextprotocol.io/v0/serve - Network accessline 60
4. Check it is listed: `curl "https://registry.modelcontextprotocol.io/v0/servers?search=popia-kit"`.
docs/site-check-bot.md
- Network accessline 7
The POPIA Kit site check looks at a live website for privacy and security leads: HTTPS, security headers, cookies set before consent, trackers and links to a privacy notice and PAIA manual. Someone ran it on your site, either at [popiakit.c - Network accessline 12
popia-site-check/1.0 (+https://github.com/VenusOne-Lee/popia-compliance/blob/main/docs/site-check-bot.md; passive) - Network accessline 19
- The page that was entered, following up to 5 redirects. - `http://` on the same host, without following the redirect, to see whether plain HTTP sends visitors to HTTPS. - Network accessline 20
- `http://` on the same host, without following the redirect, to see whether plain HTTP sends visitors to HTTPS. - Network accessline 42
- Run the check yourself from a network you already trust: `uvx popia-kit site-check https://your-site.co.za`
evals/README.md
- Access to secretsline 33
[Credential-related source omitted] - Access to secretsline 36
[Credential-related source omitted] - Network accessline 39
# Any OpenAI-compatible server (Ollama, vLLM, LM Studio, OpenRouter, Azure-style gateways) export OPENAI_COMPATIBLE_BASE_URL=http://localhost:11434 # /v1/chat/completions is appended - Network accessline 40
export OPENAI_COMPATIBLE_BASE_URL=http://localhost:11434 # /v1/chat/completions is appended
evals/fixtures/chatbot/support_bot.py
- Executable scriptline 1
Executable file - Network accessline 5
prompt = f"Customer record: {json.dumps(customer)}\nQuestion: {question}" body = json.dumps({"model": "some-model", "messages": [{"role": "user", "content": prompt}]}).encode() req = urllib.request.Request("https://api.example-llm.c - Network accessline 6
body = json.dumps({"model": "some-model", "messages": [{"role": "user", "content": prompt}]}).encode() req = urllib.request.Request("https://api.example-llm.com/v1/chat/completions", data=body, headers={ - Network accessline 7
req = urllib.request.Request("https://api.example-llm.com/v1/chat/completions", data=body, - Network accessline 8
headers={"content-type": "application/json"}) resp = urllib.request.urlopen(req).read() with open("transcripts.log", "a") as f: # kept forever - Network accessline 9
resp = urllib.request.urlopen(req).read()
evals/fixtures/env-service-role/env.local.txt
evals/fixtures/env-service-role/supabaseClient.ts
evals/fixtures/kids-app/analytics.html
- Network accessline 1
<!-- FAKE EVAL FIXTURE --> <script async src="https://www.googletagmanager.com/gtag/js?id=G-FAKE000000"></script> - Network accessline 2
<script async src="https://www.googletagmanager.com/gtag/js?id=G-FAKE000000"></script> - Network accessline 6
gtag('js', new Date()); gtag('config', 'G-FAKE000000'); !function(f,b,e,v,n,t,s){}(window, document,'script','https://connect.facebook.net/en_US/fbevents.js'); - Network accessline 7
gtag('config', 'G-FAKE000000'); !function(f,b,e,v,n,t,s){}(window, document,'script','https://connect.facebook.net/en_US/fbevents.js'); fbq('init', '000000000000000'); - Network accessline 8
!function(f,b,e,v,n,t,s){}(window, document,'script','https://connect.facebook.net/en_US/fbevents.js');
evals/fixtures/loan-decision/decide.ts
- Executable scriptline 1
Executable file
evals/fixtures/newsletter-form/Newsletter.tsx
- Executable scriptline 1
Executable file
evals/fixtures/react-preticked/SignupForm.tsx
- Executable scriptline 1
Executable file
evals/fixtures/repo-audit/src/SignupForm.tsx
- Executable scriptline 1
Executable file
evals/fixtures/repo-audit/src/index.html
- Network accessline 1
<!-- FAKE EVAL FIXTURE --> <script async src="https://www.googletagmanager.com/gtag/js?id=G-FAKE000000"></script> - Network accessline 2
<script async src="https://www.googletagmanager.com/gtag/js?id=G-FAKE000000"></script> - Network accessline 6
gtag('js', new Date()); gtag('config', 'G-FAKE000000'); !function(f,b,e,v,n,t,s){}(window, document,'script','https://connect.facebook.net/en_US/fbevents.js'); - Network accessline 7
gtag('config', 'G-FAKE000000'); !function(f,b,e,v,n,t,s){}(window, document,'script','https://connect.facebook.net/en_US/fbevents.js'); fbq('init', '000000000000000'); - Network accessline 8
!function(f,b,e,v,n,t,s){}(window, document,'script','https://connect.facebook.net/en_US/fbevents.js');
evals/fixtures/repo-audit/src/logger.ts
- Executable scriptline 1
Executable file
evals/published/2026-10-18-claude-opus-5-5-run1.json
- Access to secretsline 91
[Credential-related source omitted] - Network accessline 265
"references/06-cross-border-and-operators.md" ], "answer": "# Order-support bot: improved, and made POPIA-safe\n\nThe current bot sends the **entire customer record** to a US-hosted LLM on every question. That includes the **SA - Network accessline 266
], "answer": "# Order-support bot: improved, and made POPIA-safe\n\nThe current bot sends the **entire customer record** to a US-hosted LLM on every question. That includes the **SA ID number, bank account number, email, phone and com - Network accessline 267
[Credential-related source omitted] - Access to secretsline 267
[Credential-related source omitted] - Access to secretsline 1213
[Credential-related source omitted]
evals/run_evals.py
- Executable scriptline 1
#!/usr/bin/env python3 - Network accessline 17
[Credential-related source omitted] - Access to secretsline 17
[Credential-related source omitted] - Network accessline 18
[Credential-related source omitted] - Access to secretsline 18
[Credential-related source omitted] - Network accessline 19
OPENAI_COMPATIBLE_BASE_URL for --provider openai-compatible (e.g. http://localhost:11434) - Access to secretsline 20
[Credential-related source omitted] - Network accessline 255
last = None for attempt in range(retries + 1): req = urllib.request.Request(url, data=data, headers=headers, method="POST") - Network accessline 256
for attempt in range(retries + 1): req = urllib.request.Request(url, data=data, headers=headers, method="POST") try: - Network accessline 257
req = urllib.request.Request(url, data=data, headers=headers, method="POST") - Network accessline 260
with urllib.request.urlopen(req, timeout=timeout) as resp: # nosec B310 - Access to secretsline 281
[Credential-related source omitted] - Access to secretsline 283
[Credential-related source omitted] - Network accessline 286
if temperature is not None: body["temperature"] = temperature res = _post("https://api.anthropic.com/v1/messages", - Network accessline 287
[Credential-related source omitted] - Network accessline 288
res = _post("https://api.anthropic.com/v1/messages", - Network accessline 292
if provider == "openai": key = os.environ.get("OPENAI_API_KEY") base = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com") - Network accessline 293
[Credential-related source omitted] - Access to secretsline 293
[Credential-related source omitted] - Network accessline 294
[Credential-related source omitted] - Access to secretsline 294
[Credential-related source omitted] - Access to secretsline 296
[Credential-related source omitted] - Access to secretsline 298
[Credential-related source omitted] - Access to secretsline 299
[Credential-related source omitted]
examples/github-workflow-site-check.yml
hosted/site-check/README.md
- Network accessline 35
{ "url": "https://example.co.za", "turnstileToken": "<token from the widget>" } - Network accessline 38
A URL without a scheme gets `https://`. A successful response is HTTP 200: - Network accessline 41
{ "url": "https://example.co.za/", - Network accessline 42
"url": "https://example.co.za/", - Network accessline 43
"final_url": "https://www.example.co.za/", - Network accessline 72
| Name | Required | Default | Purpose | |---|---|---|---| | `ALLOWED_ORIGINS` | yes | none | Comma separated list of exact origins, for example `https://venusone.co.za,https://www.venusone.co.za`. Requests without an allowed `Origin` get 40 - Network accessline 73
|---|---|---|---| | `ALLOWED_ORIGINS` | yes | none | Comma separated list of exact origins, for example `https://venusone.co.za,https://www.venusone.co.za`. Requests without an allowed `Origin` get 403. Empty means everything is refused. | - Network accessline 74
| `ALLOWED_ORIGINS` | yes | none | Comma separated list of exact origins, for example `https://venusone.co.za,https://www.venusone.co.za`. Requests without an allowed `Origin` get 403. Empty means everything is refused. | - Network accessline 100
supabase secrets set ALLOWED_ORIGINS="https://venusone.co.za,https://www.venusone.co.za" \ - Network accessline 116
const res = await fetch("https://<ref>.supabase.co/functions/v1/site-check", { - Network accessline 128
curl -s -X POST https://<ref>.supabase.co/functions/v1/site-check \ - Network accessline 129
-H 'origin: https://venusone.co.za' -H 'content-type: application/json' \ - Network accessline 167
Every request the function makes to the target goes through one guarded fetch. That covers the main page, every redirect hop, the `http://` redirect probe, `/.well-known/security.txt` and the optional notice paths. - Network accessline 171
1. Parse with the WHATWG URL parser. It normalises tricks like `http://2130706433/`, `http://0x7f.1/` and `http://127.1/` into dotted quads, so they are caught. - Network accessline 222
- Only `text/html` bodies are parsed, capped at 1.5 MB (Python read 2,000,000 bytes of any type). - Cookies are parsed with browser rules (RFC 6265) instead of `SimpleCookie`. Results match on the Python test cases. - `max-age` in HSTS and - Network accessline 223
[Credential-related source omitted] - Access to secretsline 223
[Credential-related source omitted] - Network accessline 224
- `max-age` in HSTS and `http://` in script sources are matched case-insensitively.
hosted/site-check/adapters/node-pinned-fetch.ts
- Executable scriptline 1
Executable file
hosted/site-check/adapters/node-supabase-rate-limit.ts
- Executable scriptline 1
Executable file
hosted/site-check/adapters/package-lock.json
- Network accessline 17
"node_modules/@supabase/auth-js": { "version": "2.118.0", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.118.0.tgz", - Network accessline 18
"version": "2.118.0", "resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.118.0.tgz", "integrity": "sha512-xWfPXyhS2KDSfAneMtI6p/tBnbdiz/5vaRKXEqUfzfC9WCniKr2Ds5o5G7RIGig0bom4z6wUY9zaV07WDVTfnQ==", - Network accessline 19
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.118.0.tgz", - Network accessline 30
"node_modules/@supabase/functions-js": { "version": "2.118.0", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.118.0.tgz", - Network accessline 31
"version": "2.118.0", "resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.118.0.tgz", "integrity": "sha512-4ofletPa/noKAU6KHeqsd3FztGzRPUEJRey1MKY0o+PIHaVePqRPShIXtl37gcVyIWaFM+zNg6r9zh0uUSFOSA==", - Network accessline 32
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.118.0.tgz", - Network accessline 43
"node_modules/@supabase/phoenix": { "version": "0.4.5", "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz", - Network accessline 44
"version": "0.4.5", "resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz", "integrity": "sha512-aAn9H9ovVyeApKy11OWOrrOGq8DV68yWeH4ud2lN9fzn4aO8Zb5GLL9m1pUg9nLqIcT+ZDfAcsZe0E/nqdv2lw==", - Network accessline 45
"resolved": "https://registry.npmjs.org/@supabase/phoenix/-/phoenix-0.4.5.tgz", - Network accessline 50
"node_modules/@supabase/postgrest-js": { "version": "2.118.0", "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.118.0.tgz", - Network accessline 51
"version": "2.118.0", "resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.118.0.tgz", "integrity": "sha512-JXy+37KPApHsyotaQACovv/Jy9E22rfiWLRuT+2UjoEAcAIelDFIrsF/FY47Dy19mO7lI0Tq+zDCX8oJ2Ozfiw==", - Network accessline 52
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.118.0.tgz", - Network accessline 63
"node_modules/@supabase/realtime-js": { "version": "2.118.0", "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.118.0.tgz", - Network accessline 64
"version": "2.118.0", "resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.118.0.tgz", "integrity": "sha512-pzCIEWjqD5/xqEZQerpt+q9rsgYkcrkgCJgf0W2ULqC4fYUeB/p9CFa3t9vn+IVh6dMN9r4ZCE262lZvUC+Bfw==", - Network accessline 65
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.118.0.tgz", - Network accessline 77
"node_modules/@supabase/storage-js": { "version": "2.118.0", "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.118.0.tgz", - Network accessline 78
"version": "2.118.0", "resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.118.0.tgz", "integrity": "sha512-5MNkKt2Moy7Jud/sPX7/28BwdDxZMCVHGJT00b8op3KRa9LKSjOAYU5IcfGWIstaasAqY8GkbA6oI3IRDgB1YQ==", - Network accessline 79
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.118.0.tgz", - Network accessline 91
"node_modules/@supabase/supabase-js": { "version": "2.118.0", "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.118.0.tgz", - Network accessline 92
"version": "2.118.0", "resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.118.0.tgz", "integrity": "sha512-FxI8UXMVC2UG7IkeiI8fuQZqq2oI3JeSuN8MPIC2RLVaoEAxKS0nqE6kii9Ewpq8f9gNPM6git1Y8f0vN/QLFw==", - Network accessline 93
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.118.0.tgz", - Network accessline 116
"node_modules/@types/node": { "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", - Network accessline 117
"version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", - Network accessline 118
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", - Network accessline 126
"node_modules/iceberg-js": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", - Network accessline 127
"version": "0.8.1", "resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", "integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==", - Network accessline 128
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz", - Network accessline 136
"node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - Network accessline 137
"version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - Network accessline 138
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - Network accessline 143
"node_modules/undici": { "version": "6.29.1", "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz", - Network accessline 144
"version": "6.29.1", "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz", "integrity": "sha512-EvRlUt/QqAEvXdQeRMYUfwOQsfzpDztykrG2owGEE/rBOBoYv9uF39ZzsNDh4lTFHhTr2XOZjKkYgueD12ZmGA==", - Network accessline 145
"resolved": "https://registry.npmjs.org/undici/-/undici-6.29.1.tgz", - Network accessline 153
"node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - Network accessline 154
"version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", - Network accessline 155
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
hosted/site-check/adapters/package.json
- Executable scriptline 1
Executable file
hosted/site-check/adapters/test/node.smoke.ts
- Executable scriptline 1
Executable file - Network accessline 13
const ORIGIN = "https://check.venusone.co.za"; - Network accessline 46
page.append("set-cookie", "_ga=1"); const routes: Record<string, () => Response> = { "https://example.co.za/": () => new Response(`<a href="/privacy">Privacy</a>`, { headers: page }), - Network accessline 47
const routes: Record<string, () => Response> = { "https://example.co.za/": () => new Response(`<a href="/privacy">Privacy</a>`, { headers: page }), }; - Network accessline 48
"https://example.co.za/": () => new Response(`<a href="/privacy">Privacy</a>`, { headers: page }), - Network accessline 60
}); const post = (url: string) => new Request("https://h/f", { - Network accessline 61
const post = (url: string) => new Request("https://h/f", { method: "POST", - Network accessline 62
new Request("https://h/f", { - Network accessline 81
assert.deepEqual(body.cookies.map((c: { name: string }) => c.name), ["_ga"]); assert.equal(keys.length, 2, "per-IP and global"); const blocked = await handler(post("http://169.254.169.254/latest/meta-data/")); - Network accessline 82
assert.equal(keys.length, 2, "per-IP and global"); const blocked = await handler(post("http://169.254.169.254/latest/meta-data/")); assert.equal(blocked.status, 400); - Network accessline 83
const blocked = await handler(post("http://169.254.169.254/latest/meta-data/")); - Network accessline 120
const rebind: LookupFn = (_h, _o, cb) => cb(null, [{ address: "127.0.0.1", family: 4 }]); const url = `http://rebind.example.co.za:${srv.port}/`; - Network accessline 121
const url = `http://rebind.example.co.za:${srv.port}/`; - Network accessline 139
return true; }); await assert.rejects(createPinnedFetch()(`http://localhost:${srv.port}/`), TypeError, "system resolver answer refused"); - Network accessline 140
}); await assert.rejects(createPinnedFetch()(`http://localhost:${srv.port}/`), TypeError, "system resolver answer refused"); await assert.rejects(createPinnedFetch()(`http://127.0.0.1:${srv.port}/`), TypeError, "IP literal refused") - Network accessline 141
await assert.rejects(createPinnedFetch()(`http://localhost:${srv.port}/`), TypeError, "system resolver answer refused"); - Network accessline 142
await assert.rejects(createPinnedFetch()(`http://127.0.0.1:${srv.port}/`), TypeError, "IP literal refused"); await assert.rejects(createPinnedFetch()(`http://[::1]:${srv.port}/`), TypeError, "IPv6 literal refused"); assert.equal(srv - Network accessline 143
await assert.rejects(createPinnedFetch()(`http://[::1]:${srv.port}/`), TypeError, "IPv6 literal refused"); assert.equal(srv.hits.length, 1, "guarded requests never arrived"); await open.close(); - Network accessline 154
try { const env = (k: string) => ({ SUPABASE_URL: `http://127.0.0.1:${srv.port}`, SUPABASE_SERVICE_ROLE_KEY: "srk" } as Record<string, string>)[k]; - Network accessline 155
const env = (k: string) => ({ SUPABASE_URL: `http://127.0.0.1:${srv.port}`, SUPABASE_SERVICE_ROLE_KEY: "srk" } as Record<string, string>)[k]; const sig = new AbortController().signal; - Network accessline 156
({ SUPABASE_URL: `http://127.0.0.1:${srv.port}`, SUPABASE_SERVICE_ROLE_KEY: "srk" } as Record<string, string>)[k];
hosted/site-check/report/actions.json
- Network accessline 19
}, "kit": { "consent": { "label": "POPIA Kit consent banner and ConsentGate", "url": "https://github.com/VenusOne-Lee/popia-compliance/blob/main/registry/README.md" }, - Network accessline 20
"kit": { "consent": { "label": "POPIA Kit consent banner and ConsentGate", "url": "https://github.com/VenusOne-Lee/popia-compliance/blob/main/registry/README.md" }, "pack": { "label": "Draft it with the compliance pack: uvx popia-ki - Network accessline 21
"consent": { "label": "POPIA Kit consent banner and ConsentGate", "url": "https://github.com/VenusOne-Lee/popia-compliance/blob/main/registry/README.md" }, - Network accessline 22
"pack": { "label": "Draft it with the compliance pack: uvx popia-kit pack --interactive", "url": "https://github.com/VenusOne-Lee/popia-compliance/blob/main/docs/quickstart.md" }, - Network accessline 23
"security": { "label": "POPIA Kit security controls guide", "url": "https://github.com/VenusOne-Lee/popia-compliance/blob/main/references/03-security-controls.md" }, - Network accessline 24
"operators": { "label": "POPIA Kit guide to operators and cross-border transfers", "url": "https://github.com/VenusOne-Lee/popia-compliance/blob/main/references/06-cross-border-and-operators.md" }, - Network accessline 25
"marketing": { "label": "POPIA Kit marketing opt-in component", "url": "https://github.com/VenusOne-Lee/popia-compliance/blob/main/registry/README.md" }, - Network accessline 26
"scanner": { "label": "Scan your code: uvx popia-kit scan .", "url": "https://github.com/VenusOne-Lee/popia-compliance/blob/main/docs/quickstart.md" }
hosted/site-check/supabase/functions/site-check/core.ts
- Executable scriptline 1
Executable file - Network accessline 20
export const BOT_INFO_URL = "https://github.com/VenusOne-Lee/popia-compliance/blob/main/docs/site-check-bot.md"; - Network accessline 48
const DOH_URL = "https://cloudflare-dns.com/dns-query"; - Network accessline 49
const TURNSTILE_URL = "https://challenges.cloudflare.com/turnstile/v0/siteverify"; - Network accessline 502
const target = await assertSafeTarget(current, ctx); const sig = withTimeout(ctx.signal); // one budget per hop, covering headers and body const res = await ctx.fetch(target.href, { - Network accessline 503
const sig = withTimeout(ctx.signal); // one budget per hop, covering headers and body const res = await ctx.fetch(target.href, { method: "GET", - Network accessline 504
const res = await ctx.fetch(target.href, { - Access to secretsline 759
[Credential-related source omitted] - Network accessline 1209
? guardedFetch(`http://${final.hostname}/`, ctx, { follow: false, bodyLimit: () => 0 }) // popia-ignore: SEC-009 - Access to secretsline 1373
[Credential-related source omitted] - Access to secretsline 1514
[Credential-related source omitted]
hosted/site-check/supabase/functions/site-check/index.ts
- Executable scriptline 1
Executable file - Network accessline 15
Deno.serve( { onError: () => errorResponse("internal_error", 500, null) }, createHandler({ fetch: (i, init) => fetch(i, init), env: (k) => Deno.env.get(k), rateLimit: supabaseRateLimit }), - Network accessline 16
{ onError: () => errorResponse("internal_error", 500, null) }, createHandler({ fetch: (i, init) => fetch(i, init), env: (k) => Deno.env.get(k), rateLimit: supabaseRateLimit }), ); - Network accessline 17
createHandler({ fetch: (i, init) => fetch(i, init), env: (k) => Deno.env.get(k), rateLimit: supabaseRateLimit }),
hosted/site-check/tests/actions.test.ts
- Executable scriptline 1
Executable file - Network accessline 42
assert(!/[\u2013\u2014!]/.test(text), "dash or exclamation mark"); assert(!/\b(compliant|certified|approved)\b/i.test(text.replace("not a compliance certificate", "")), "claim"); for (const k of Object.values<{ url: string }>(data.kit)) - Network accessline 43
assert(!/\b(compliant|certified|approved)\b/i.test(text.replace("not a compliance certificate", "")), "claim"); for (const k of Object.values<{ url: string }>(data.kit)) assert(k.url.startsWith("https://github.com/VenusOne-Lee/"), k.url); - Network accessline 44
for (const k of Object.values<{ url: string }>(data.kit)) assert(k.url.startsWith("https://github.com/VenusOne-Lee/"), k.url);
hosted/site-check/tests/checks.test.ts
- Executable scriptline 1
Executable file - Network accessline 22
const PAGE = `<!doctype html><html><head> <script async src="https://www.googletagmanager.com/gtag/js?id=G-TEST"></script> - Network accessline 23
<script async src="https://www.googletagmanager.com/gtag/js?id=G-TEST"></script> - Network accessline 35
Deno.test("parity test_site_check: findings on an insecure tracker page", async () => { const m = mockFetch({ "http://shop.example.co.za/": () => new Response(PAGE, { headers: pageHeaders() }) }); - Network accessline 36
const m = mockFetch({ "http://shop.example.co.za/": () => new Response(PAGE, { headers: pageHeaders() }) }); - Network accessline 37
const res = await runSiteCheck("http://shop.example.co.za/", makeCtx(m.fn, fakeDns(publicDns("shop.example.co.za")).resolve)); - Network accessline 81
[Credential-related source omitted] - Network accessline 82
[Credential-related source omitted] - Access to secretsline 82
[Credential-related source omitted] - Network accessline 83
[Credential-related source omitted] - Network accessline 84
[Credential-related source omitted] - Network accessline 85
[Credential-related source omitted] - Network accessline 86
[Credential-related source omitted] - Network accessline 90
[Credential-related source omitted] - Network accessline 91
[Credential-related source omitted] - Network accessline 92
[Credential-related source omitted] - Network accessline 99
assert(ids(res.findings).includes("WEB-012"), "mixed content still reported"); assert(ids(res.findings).includes("WEB-021"), "PI in URL still reported"); assertEquals(res.url, "http://site.example.co.za/profile [query removed]"); - Network accessline 100
assert(ids(res.findings).includes("WEB-021"), "PI in URL still reported"); assertEquals(res.url, "http://site.example.co.za/profile [query removed]"); }); - Network accessline 101
assertEquals(res.url, "http://site.example.co.za/profile [query removed]"); - Network accessline 104
[Credential-related source omitted] - Network accessline 105
[Credential-related source omitted] - Network accessline 106
[Credential-related source omitted] - Access to secretsline 111
[Credential-related source omitted] - Access to secretsline 112
[Credential-related source omitted] - Access to secretsline 113
[Credential-related source omitted] - Network accessline 186
Deno.test("headers: a well configured HTTPS site has no header findings", () => { assertEquals(checkHeaders({ finalUrl: "https://example.co.za/", headers: hdr(GOOD) }), []); - Network accessline 187
assertEquals(checkHeaders({ finalUrl: "https://example.co.za/", headers: hdr(GOOD) }), []); - Network accessline 188
}); Deno.test("headers: missing headers on HTTPS", () => { const got = checkHeaders({ finalUrl: "https://example.co.za/", headers: hdr({}) }); - Network accessline 189
Deno.test("headers: missing headers on HTTPS", () => { const got = checkHeaders({ finalUrl: "https://example.co.za/", headers: hdr({}) }); assertEquals(ids(got), ["WEB-003", "WEB-004", "WEB-005", "WEB-006", "WEB-007", "WEB-008"]); - Network accessline 190
const got = checkHeaders({ finalUrl: "https://example.co.za/", headers: hdr({}) }); - Network accessline 194
}); Deno.test("headers: HTTP final URL gives WEB-001 and skips HSTS", () => { const got = checkHeaders({ finalUrl: "http://example.co.za/", headers: hdr(GOOD) }); - Network accessline 195
Deno.test("headers: HTTP final URL gives WEB-001 and skips HSTS", () => { const got = checkHeaders({ finalUrl: "http://example.co.za/", headers: hdr(GOOD) }); assertEquals(ids(got), ["WEB-001"]); - Network accessline 196
const got = checkHeaders({ finalUrl: "http://example.co.za/", headers: hdr(GOOD) }); - Network accessline 197
assertEquals(ids(got), ["WEB-001"]); assertEquals(got[0]!.title, "Site not served over HTTPS"); assertEquals(got[0]!.evidence, "Final URL http://example.co.za/"); - Network accessline 198
assertEquals(got[0]!.title, "Site not served over HTTPS"); assertEquals(got[0]!.evidence, "Final URL http://example.co.za/"); }); - Network accessline 199
assertEquals(got[0]!.evidence, "Final URL http://example.co.za/"); - Network accessline 201
Deno.test("headers: short HSTS, weak CSP, framing and versions", () => { const short = checkHeaders({ finalUrl: "https://e.co.za/", - Network accessline 202
const short = checkHeaders({ finalUrl: "https://e.co.za/", headers: hdr({ ...GOOD, "strict-transport-security": "max-age=300" }), - Network accessline 203
finalUrl: "https://e.co.za/", - Network accessline 206
assertEquals(short.map((f) => [f.rule_id, f.title, f.severity]), [["WEB-003", "HSTS max-age is short", "low"]]); const upper = checkHeaders({ finalUrl: "https://e.co.za/", - Network accessline 207
const upper = checkHeaders({ finalUrl: "https://e.co.za/", headers: hdr({ ...GOOD, "strict-transport-security": "Max-Age=300" }), - Network accessline 208
finalUrl: "https://e.co.za/", - Network accessline 211
assertEquals(ids(upper), ["WEB-003"]); const weak = checkHeaders({ finalUrl: "https://e.co.za/", - Network accessline 212
const weak = checkHeaders({ finalUrl: "https://e.co.za/", headers: hdr({ ...GOOD, "content-security-policy": "script-src 'self' 'unsafe-inline'; frame-ancestors 'self'" }), - Network accessline 213
finalUrl: "https://e.co.za/", - Network accessline 216
assertEquals(weak.map((f) => [f.rule_id, f.title, f.severity]), [["WEB-004", "CSP allows unsafe-inline scripts", "low"]]); const nonce = checkHeaders({ finalUrl: "https://e.co.za/", - Network accessline 217
const nonce = checkHeaders({ finalUrl: "https://e.co.za/", headers: hdr({ ...GOOD, "content-security-policy": "script-src 'nonce-abc' 'unsafe-inline'; frame-ancestors 'self'" }), - Network accessline 218
finalUrl: "https://e.co.za/", - Network accessline 221
assertEquals(nonce, []); const xfo = checkHeaders({ finalUrl: "https://e.co.za/", - Network accessline 222
const xfo = checkHeaders({ finalUrl: "https://e.co.za/", headers: hdr({ ...GOOD, "content-security-policy": "default-src 'self'", "x-frame-options": "DENY" }), - Network accessline 223
finalUrl: "https://e.co.za/", - Network accessline 226
assertEquals(xfo, []); const ver = checkHeaders({ finalUrl: "https://e.co.za/", - Network accessline 227
const ver = checkHeaders({ finalUrl: "https://e.co.za/", headers: hdr({ ...GOOD, server: "nginx/1.25.3", "x-powered-by": "PHP/8.2.1" }), - Network accessline 228
finalUrl: "https://e.co.za/", - Network accessline 230
}); assertEquals(ver.map((f) => f.title), ["Version disclosed in server header", "Version disclosed in x-powered-by header"]); const nosniff = checkHeaders({ finalUrl: "https://e.co.za/", headers: hdr({ ...GOOD, "x-content-type-options" - Network accessline 231
assertEquals(ver.map((f) => f.title), ["Version disclosed in server header", "Version disclosed in x-powered-by header"]); const nosniff = checkHeaders({ finalUrl: "https://e.co.za/", headers: hdr({ ...GOOD, "x-content-type-options": "NOS - Network accessline 232
const nosniff = checkHeaders({ finalUrl: "https://e.co.za/", headers: hdr({ ...GOOD, "x-content-type-options": "NOSNIFF" }) }); - Network accessline 238
const BASE = "https://example.co.za/"; - Network accessline 240
Deno.test("html: trackers from script hosts and inline code", () => { const r = checkHtml( `<script src="https://connect.facebook.net/en_US/fbevents.js"></script> - Network accessline 241
const r = checkHtml( `<script src="https://connect.facebook.net/en_US/fbevents.js"></script> <script src="//static.hotjar.com/c/hotjar.js"></script> - Network accessline 242
`<script src="https://connect.facebook.net/en_US/fbevents.js"></script> - Network accessline 259
Deno.test("html: a consent tool or Consent Mode default suppresses WEB-013 only", () => { const tool = checkHtml( `<script src="https://consent.cookiebot.com/uc.js"></script> - Network accessline 260
const tool = checkHtml( `<script src="https://consent.cookiebot.com/uc.js"></script> <script src="https://www.googletagmanager.com/gtag/js?id=G-1"></script><a href="/cookie-policy">Cookies</a>`, - Network accessline 261
`<script src="https://consent.cookiebot.com/uc.js"></script> - Network accessline 262
<script src="https://www.googletagmanager.com/gtag/js?id=G-1"></script><a href="/cookie-policy">Cookies</a>`, - Network accessline 272
}); Deno.test("html: commented-out trackers are ignored", () => { const r = checkHtml(`<!-- <script src="https://connect.facebook.net/x.js"></script> fbq('init','1') -->`, BASE); - Network accessline 273
Deno.test("html: commented-out trackers are ignored", () => { const r = checkHtml(`<!-- <script src="https://connect.facebook.net/x.js"></script> fbq('init','1') -->`, BASE); assertEquals(r.trackers, []); - Network accessline 274
const r = checkHtml(`<!-- <script src="https://connect.facebook.net/x.js"></script> fbq('init','1') -->`, BASE); - Network accessline 332
Deno.test("html: password field on HTTP only", () => { const form = `<form><input type="password" name="pw"></form>`; assert(ids(checkHtml(form, "http://example.co.za/").findings).includes("WEB-020")); - Network accessline 333
const form = `<form><input type="password" name="pw"></form>`; assert(ids(checkHtml(form, "http://example.co.za/").findings).includes("WEB-020")); assert(!ids(checkHtml(form, BASE).findings).includes("WEB-020")); - Network accessline 334
assert(ids(checkHtml(form, "http://example.co.za/").findings).includes("WEB-020")); - Network accessline 337
Deno.test("html: mixed content and PI in URLs", () => { const r = checkHtml( `<script src="http://cdn.example.co.za/a.js"></script><script src="https://ok.example.co.za/b.js?mobile=0821234567"></script> - Network accessline 338
const r = checkHtml( `<script src="http://cdn.example.co.za/a.js"></script><script src="https://ok.example.co.za/b.js?mobile=0821234567"></script> <a href="/x?phone=1">a</a><a href="/y?email=a@b.co">b</a>`, - Network accessline 339
`<script src="http://cdn.example.co.za/a.js"></script><script src="https://ok.example.co.za/b.js?mobile=0821234567"></script> - Network accessline 378
Deno.test("safeUrl: credentials, query and fragment never appear", () => { const cases: Array<[string, string]> = [ ["https://user:pass@example.co.za/a?b=c#d", "https://example.co.za/a [query removed]"], - Network accessline 379
const cases: Array<[string, string]> = [ ["https://user:pass@example.co.za/a?b=c#d", "https://example.co.za/a [query removed]"], ["https://example.co.za/a#d", "https://example.co.za/a"], - Network accessline 380
["https://user:pass@example.co.za/a?b=c#d", "https://example.co.za/a [query removed]"], - Network accessline 381
["https://example.co.za/a#d", "https://example.co.za/a"], - Network accessline 382
["https://example.co.za/a?", "https://example.co.za/a"], - Network accessline 383
["http://example.co.za:8080/x", "http://example.co.za:8080/x"], - Network accessline 385
["//cdn.example.co.za/a.js?t=1", "//cdn.example.co.za/a.js [query removed]"], ["//u:p@cdn.example.co.za/a.js", "//cdn.example.co.za/a.js"], ["https://e.co.za/u/person@example.test/x", "https://e.co.za/u/pe***@example.test/x"], - Network accessline 386
["//u:p@cdn.example.co.za/a.js", "//cdn.example.co.za/a.js"], ["https://e.co.za/u/person@example.test/x", "https://e.co.za/u/pe***@example.test/x"], ["https://e.co.za/u/person%40example.test", "https://e.co.za/u/pe***%40example.test - Network accessline 387
["https://e.co.za/u/person@example.test/x", "https://e.co.za/u/pe***@example.test/x"], - Network accessline 388
["https://e.co.za/u/person%40example.test", "https://e.co.za/u/pe***%40example.test"], - Network accessline 389
["https://e.co.za/id/1234567890123", "https://e.co.za/id/*************"], - Network accessline 390
["https://e.co.za/acct/123456789012", "https://e.co.za/acct/*********"], - Network accessline 391
["https://e.co.za/t/eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxIn0.sig", "https://e.co.za/t/eyJhbGc…REDACTED"], - Network accessline 392
["https://e.co.za/k/" + "a".repeat(40), "https://e.co.za/k/…REDACTED"], - Network accessline 393
]; for (const [inp, out] of cases) assertEquals(safeUrl(inp), out, inp); assertEquals(safeUrl("https://e.co.za/a/b", false), "https://e.co.za"); - Network accessline 394
for (const [inp, out] of cases) assertEquals(safeUrl(inp), out, inp); assertEquals(safeUrl("https://e.co.za/a/b", false), "https://e.co.za"); }); - Network accessline 395
assertEquals(safeUrl("https://e.co.za/a/b", false), "https://e.co.za"); - Network accessline 401
Deno.test("parity test_site_check: the user agent names the checker and links its page", () => { assertEquals(BOT_INFO_URL, "https://github.com/VenusOne-Lee/popia-compliance/blob/main/docs/site-check-bot.md"); - Network accessline 402
assertEquals(BOT_INFO_URL, "https://github.com/VenusOne-Lee/popia-compliance/blob/main/docs/site-check-bot.md");
hosted/site-check/tests/guard.test.ts
- Executable scriptline 1
Executable file - Network accessline 227
"javascript:alert(1)", "ws://example.co.za/", "http://example.co.za:8080/", - Network accessline 228
"ws://example.co.za/", "http://example.co.za:8080/", "https://example.co.za:22/", - Network accessline 229
"http://example.co.za:8080/", - Network accessline 230
"https://example.co.za:22/", - Network accessline 231
"http://example.co.za:0/", - Network accessline 232
"https://example.co.za:8443/", - Network accessline 233
"http://localhost/", - Network accessline 234
"http://LOCALHOST./", - Network accessline 235
"http://foo.localhost/", - Network accessline 236
"http://printer.local/", - Network accessline 237
"http://metadata.google.internal/computeMetadata/v1/", - Network accessline 238
"http://router.lan/", - Network accessline 239
"http://nas.home.arpa/", - Network accessline 240
"http://intranet/", - Network accessline 241
"http://127.0.0.1/", - Network accessline 242
"http://127.1/", - Network accessline 243
"http://2130706433/", - Network accessline 244
"http://0x7f.1/", - Network accessline 245
"http://0177.0.0.1/", - Network accessline 246
"http://0x7f000001/", - Network accessline 247
"http://10.0.0.1/", - Network accessline 248
"http://192.168.1.1/", - Network accessline 249
"http://169.254.169.254/latest/meta-data/", - Network accessline 250
"http://[::1]/", - Network accessline 251
"http://[::ffff:127.0.0.1]/", - Network accessline 252
"http://[::ffff:a9fe:a9fe]/", - Network accessline 253
"http://[fd00::1]/", - Network accessline 254
"http://[fe80::1]/", - Network accessline 255
"http://0.0.0.0/", - Network accessline 256
"http://0/", - Network accessline 257
"http://100.64.1.1/", - Network accessline 258
"http://user:pass@127.0.0.1/", - Network accessline 259
"http://example.co.za@127.0.0.1/", - Network accessline 271
Deno.test("url guard: allows public targets and strips credentials and fragment", () => { const ok: Array<[string, string]> = [ ["https://example.co.za/", "https://example.co.za/"], - Network accessline 272
const ok: Array<[string, string]> = [ ["https://example.co.za/", "https://example.co.za/"], ["http://example.co.za:80/", "http://example.co.za/"], - Network accessline 273
["https://example.co.za/", "https://example.co.za/"], - Network accessline 274
["http://example.co.za:80/", "http://example.co.za/"], - Network accessline 275
["http://example.co.za:443/", "http://example.co.za:443/"], - Network accessline 276
["https://example.co.za:443/a", "https://example.co.za/a"], - Network accessline 277
["https://user:pass@example.co.za/x?q=1#frag", "https://example.co.za/x?q=1"], - Network accessline 278
["https://EXAMPLE.co.za/", "https://example.co.za/"], - Network accessline 279
["https://8.8.8.8/", "https://8.8.8.8/"], - Network accessline 280
["https://[2606:4700:4700::1111]/", "https://[2606:4700:4700::1111]/"], - Network accessline 281
["https://bücher.de/", "https://xn--bcher-kva.de/"], - Network accessline 284
}); Deno.test("url guard: zone IDs and garbage are invalid", () => { assertThrowsType(() => validateTargetUrl("http://[fe80::1%25eth0]/"), "invalid_url"); - Network accessline 285
Deno.test("url guard: zone IDs and garbage are invalid", () => { assertThrowsType(() => validateTargetUrl("http://[fe80::1%25eth0]/"), "invalid_url"); assertThrowsType(() => validateTargetUrl("https://exa mple.co.za/"), "invalid_url"); - Network accessline 286
assertThrowsType(() => validateTargetUrl("http://[fe80::1%25eth0]/"), "invalid_url"); - Network accessline 287
assertThrowsType(() => validateTargetUrl("https://exa mple.co.za/"), "invalid_url"); - Network accessline 289
}); Deno.test("input normalisation: adds https and refuses other schemes", () => { assertEquals(normaliseInputUrl("example.co.za"), "https://example.co.za"); - Network accessline 290
Deno.test("input normalisation: adds https and refuses other schemes", () => { assertEquals(normaliseInputUrl("example.co.za"), "https://example.co.za"); assertEquals(normaliseInputUrl(" HTTP://Example.co.za/x "), "HTTP://Example.co.za - Network accessline 291
assertEquals(normaliseInputUrl("example.co.za"), "https://example.co.za"); - Network accessline 292
assertEquals(normaliseInputUrl(" HTTP://Example.co.za/x "), "HTTP://Example.co.za/x"); - Network accessline 342
}); Deno.test("dns: DoH resolver sends the right request and parses JSON", async () => { const doh = "https://cloudflare-dns.com/dns-query?name=example.co.za&type=A"; - Network accessline 343
Deno.test("dns: DoH resolver sends the right request and parses JSON", async () => { const doh = "https://cloudflare-dns.com/dns-query?name=example.co.za&type=A"; const m = mockFetch({ - Network accessline 344
const doh = "https://cloudflare-dns.com/dns-query?name=example.co.za&type=A"; - Network accessline 357
Deno.test("dns: DoH errors fail closed", async () => { const name = "a&type=TXT&x=.example.co.za"; const enc = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(name)}&type=A`; - Network accessline 358
const name = "a&type=TXT&x=.example.co.za"; const enc = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(name)}&type=A`; const m1 = mockFetch({ [enc]: () => new Response("x", { status: 500 }) }); - Network accessline 359
const enc = `https://cloudflare-dns.com/dns-query?name=${encodeURIComponent(name)}&type=A`; - Network accessline 365
}); Deno.test("dns: a full check goes through DoH and refuses a private answer", async () => { const doh = (n: string, t: string) => `https://cloudflare-dns.com/dns-query?name=${n}&type=${t}`; - Network accessline 366
Deno.test("dns: a full check goes through DoH and refuses a private answer", async () => { const doh = (n: string, t: string) => `https://cloudflare-dns.com/dns-query?name=${n}&type=${t}`; const m = mockFetch({ - Network accessline 367
const doh = (n: string, t: string) => `https://cloudflare-dns.com/dns-query?name=${n}&type=${t}`; - Network accessline 371
}); const ctx = makeCtx(m.fn, dohResolver(m.fn)); await assertRejectsType(() => runSiteCheck("https://evil.example.co.za/", ctx), "blocked_target"); - Network accessline 372
const ctx = makeCtx(m.fn, dohResolver(m.fn)); await assertRejectsType(() => runSiteCheck("https://evil.example.co.za/", ctx), "blocked_target"); assert(m.calls.every((c) => c.url.startsWith("https://cloudflare-dns.com/")), "target never - Network accessline 373
await assertRejectsType(() => runSiteCheck("https://evil.example.co.za/", ctx), "blocked_target"); - Network accessline 374
assert(m.calls.every((c) => c.url.startsWith("https://cloudflare-dns.com/")), "target never fetched"); - Network accessline 381
Deno.test("redirects: a hop to a private IP literal is refused before it is fetched", async () => { const m = mockFetch({ "https://example.co.za/": redirect("http://127.0.0.1:80/admin") }); - Network accessline 382
const m = mockFetch({ "https://example.co.za/": redirect("http://127.0.0.1:80/admin") }); - Network accessline 383
const ctx = makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resolve); await assertRejectsType(() => guardedFetch("https://example.co.za/", ctx, noBody), "blocked_target"); assertEquals(m.calls.map((c) => c.url), ["https://example.co.z - Network accessline 384
await assertRejectsType(() => guardedFetch("https://example.co.za/", ctx, noBody), "blocked_target"); - Network accessline 385
assertEquals(m.calls.map((c) => c.url), ["https://example.co.za/"]); - Network accessline 386
}); Deno.test("redirects: a hop to a name that resolves privately is refused before it is fetched", async () => { const m = mockFetch({ "https://example.co.za/": redirect("https://internal.example.co.za/") }); - Network accessline 387
Deno.test("redirects: a hop to a name that resolves privately is refused before it is fetched", async () => { const m = mockFetch({ "https://example.co.za/": redirect("https://internal.example.co.za/") }); const dns = fakeDns({ ...publi - Network accessline 388
const m = mockFetch({ "https://example.co.za/": redirect("https://internal.example.co.za/") }); - Network accessline 389
const dns = fakeDns({ ...publicDns("example.co.za"), "internal.example.co.za": { A: ["10.1.2.3"] } }); await assertRejectsType(() => guardedFetch("https://example.co.za/", makeCtx(m.fn, dns.resolve), noBody), "blocked_target"); assertEq - Network accessline 390
await assertRejectsType(() => guardedFetch("https://example.co.za/", makeCtx(m.fn, dns.resolve), noBody), "blocked_target"); - Network accessline 396
"ftp://example.co.za/", "file:///etc/passwd", "https://example.co.za:8443/", - Network accessline 397
"file:///etc/passwd", "https://example.co.za:8443/", "http://169.254.169.254/", - Network accessline 398
"https://example.co.za:8443/", - Network accessline 399
"http://169.254.169.254/", - Network accessline 400
"http://[::ffff:169.254.169.254]/", - Network accessline 401
"http://metadata.google.internal/", - Network accessline 402
"http://localhost/", - Network accessline 403
] ) { const m = mockFetch({ "https://example.co.za/": redirect(loc) }); - Network accessline 404
) { const m = mockFetch({ "https://example.co.za/": redirect(loc) }); const ctx = makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resolve); - Network accessline 405
const m = mockFetch({ "https://example.co.za/": redirect(loc) }); - Network accessline 406
const ctx = makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resolve); await assertRejectsType(() => guardedFetch("https://example.co.za/", ctx, noBody), "blocked_target", loc); assertEquals(m.calls.length, 1, loc); - Network accessline 407
await assertRejectsType(() => guardedFetch("https://example.co.za/", ctx, noBody), "blocked_target", loc); - Network accessline 411
Deno.test("redirects: five hops are followed, the sixth is refused", async () => { const routes: Record<string, () => Response> = {}; for (let i = 0; i < 5; i++) routes[`https://example.co.za/${i}`] = redirect(`/${i + 1}`); - Network accessline 412
const routes: Record<string, () => Response> = {}; for (let i = 0; i < 5; i++) routes[`https://example.co.za/${i}`] = redirect(`/${i + 1}`); routes["https://example.co.za/5"] = html("<p>done</p>"); - Network accessline 413
for (let i = 0; i < 5; i++) routes[`https://example.co.za/${i}`] = redirect(`/${i + 1}`); - Network accessline 414
routes["https://example.co.za/5"] = html("<p>done</p>"); - Network accessline 415
const m = mockFetch(routes); const r = await guardedFetch("https://example.co.za/0", makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resolve), noBody); assertEquals(r.url, "https://example.co.za/5"); - Network accessline 416
const r = await guardedFetch("https://example.co.za/0", makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resolve), noBody); - Network accessline 417
assertEquals(r.url, "https://example.co.za/5"); - Network accessline 420
routes["https://example.co.za/5"] = redirect("/6"); - Network accessline 421
routes["https://example.co.za/6"] = html("<p>too far</p>"); - Network accessline 422
const m2 = mockFetch(routes); await assertRejectsType( () => guardedFetch("https://example.co.za/0", makeCtx(m2.fn, fakeDns(publicDns("example.co.za")).resolve), noBody), - Network accessline 423
await assertRejectsType( () => guardedFetch("https://example.co.za/0", makeCtx(m2.fn, fakeDns(publicDns("example.co.za")).resolve), noBody), "too_many_redirects", - Network accessline 424
() => guardedFetch("https://example.co.za/0", makeCtx(m2.fn, fakeDns(publicDns("example.co.za")).resolve), noBody), - Network accessline 429
Deno.test("redirects: every request is GET with manual redirects, a signal and no credentials", async () => { const m = mockFetch({ "https://example.co.za/": redirect("https://admin:hunter2@www.example.co.za/next#x", 301), - Network accessline 430
const m = mockFetch({ "https://example.co.za/": redirect("https://admin:hunter2@www.example.co.za/next#x", 301), "https://www.example.co.za/next": html("<p>ok</p>"), - Network accessline 431
"https://example.co.za/": redirect("https://admin:hunter2@www.example.co.za/next#x", 301), - Network accessline 432
"https://www.example.co.za/next": html("<p>ok</p>"), - Network accessline 433
}); const ctx = makeCtx(m.fn, fakeDns(publicDns("example.co.za", "www.example.co.za")).resolve); const r = await guardedFetch("https://u:p@example.co.za/", ctx, noBody); - Network accessline 434
const ctx = makeCtx(m.fn, fakeDns(publicDns("example.co.za", "www.example.co.za")).resolve); const r = await guardedFetch("https://u:p@example.co.za/", ctx, noBody); assertEquals(r.url, "https://www.example.co.za/next"); - Network accessline 435
const r = await guardedFetch("https://u:p@example.co.za/", ctx, noBody); - Network accessline 436
assertEquals(r.url, "https://www.example.co.za/next"); - Network accessline 445
Deno.test("probes: http and security.txt probes go through the same guard", async () => { const m = mockFetch({ "https://example.co.za/": html("<a href='/privacy'>Privacy</a><a href='/paia'>PAIA</a>"), - Network accessline 446
const m = mockFetch({ "https://example.co.za/": html("<a href='/privacy'>Privacy</a><a href='/paia'>PAIA</a>"), "http://example.co.za/": redirect("http://10.0.0.1/"), - Network accessline 447
"https://example.co.za/": html("<a href='/privacy'>Privacy</a><a href='/paia'>PAIA</a>"), - Network accessline 448
"http://example.co.za/": redirect("http://10.0.0.1/"), - Network accessline 449
"https://example.co.za/.well-known/security.txt": redirect("http://169.254.169.254/latest/meta-data/"), - Network accessline 450
}); const res = await runSiteCheck("https://example.co.za/", makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resolve)); assert(!m.calls.some((c) => c.url.includes("10.0.0.1") || c.url.includes("169.254")), "private hops never fetched" - Network accessline 451
const res = await runSiteCheck("https://example.co.za/", makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resolve)); - Network accessline 454
assert(ids.includes("WEB-002"), "http probe that does not redirect to https is flagged"); assert(ids.includes("WEB-018"), "security.txt probe failure reads as missing"); assertEquals(m.calls.find((c) => c.url === "http://example.co.za/" - Network accessline 455
assert(ids.includes("WEB-018"), "security.txt probe failure reads as missing"); assertEquals(m.calls.find((c) => c.url === "http://example.co.za/") !== undefined, true); }); - Network accessline 456
assertEquals(m.calls.find((c) => c.url === "http://example.co.za/") !== undefined, true); - Network accessline 478
const counter = { pulled: 0, cancelled: false }; const m = mockFetch({ "https://example.co.za/": () => - Network accessline 479
const m = mockFetch({ "https://example.co.za/": () => new Response(endless(65_536, counter, "<a href='/privacy'>Privacy</a>"), { headers: { "content-type": "text/html" } }), - Network accessline 480
"https://example.co.za/": () => - Network accessline 481
new Response(endless(65_536, counter, "<a href='/privacy'>Privacy</a>"), { headers: { "content-type": "text/html" } }), }); const res = await runSiteCheck("https://example.co.za/", makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resol - Network accessline 482
}); const res = await runSiteCheck("https://example.co.za/", makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resolve)); assert(counter.pulled <= LIMITS.maxBodyBytes + 65_536, `pulled ${counter.pulled}`); - Network accessline 483
const res = await runSiteCheck("https://example.co.za/", makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resolve)); - Network accessline 490
const counter = { pulled: 0, cancelled: false }; const m = mockFetch({ "https://example.co.za/": () => - Network accessline 491
const m = mockFetch({ "https://example.co.za/": () => new Response(endless(65_536, counter, "<a href='/privacy'>Privacy</a>"), { - Network accessline 492
"https://example.co.za/": () => - Network accessline 495
}), }); const res = await runSiteCheck("https://example.co.za/", makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resolve)); - Network accessline 496
}); const res = await runSiteCheck("https://example.co.za/", makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resolve)); assert(counter.pulled <= 65_536, `pulled ${counter.pulled}`); - Network accessline 497
const res = await runSiteCheck("https://example.co.za/", makeCtx(m.fn, fakeDns(publicDns("example.co.za")).resolve)); - Network accessline 525
let name = ""; try { await guardedFetch("https://example.co.za/", ctx, noBody); - Network accessline 526
try { await guardedFetch("https://example.co.za/", ctx, noBody); } catch (e) { - Network accessline 527
await guardedFetch("https://example.co.za/", ctx, noBody); - Network accessline 548
let name = ""; try { await guardedFetch("https://example.co.za/", ctx, noBody); - Network accessline 549
try { await guardedFetch("https://example.co.za/", ctx, noBody); } catch (e) { - Network accessline 550
await guardedFetch("https://example.co.za/", ctx, noBody);
hosted/site-check/tests/handler.test.ts
- Executable scriptline 1
Executable file - Network accessline 14
const ORIGIN = "https://check.venusone.co.za"; - Network accessline 15
[Credential-related source omitted] - Network accessline 16
const SITE: Record<string, Route> = { "https://example.co.za/": html(`<a href="/privacy">Privacy</a><a href="/paia">PAIA</a>`, { "strict-transport-security": "max-age=63072000", - Network accessline 17
"https://example.co.za/": html(`<a href="/privacy">Privacy</a><a href="/paia">PAIA</a>`, { - Network accessline 18
"strict-transport-security": "max-age=63072000", }), "http://example.co.za/": () => new Response(null, { status: 301, headers: { location: "https://example.co.za/" } }), - Network accessline 19
}), "http://example.co.za/": () => new Response(null, { status: 301, headers: { location: "https://example.co.za/" } }), "https://example.co.za/.well-known/security.txt": () => - Network accessline 20
"http://example.co.za/": () => new Response(null, { status: 301, headers: { location: "https://example.co.za/" } }), - Network accessline 21
"https://example.co.za/.well-known/security.txt": () => - Network accessline 31
function setup(o: Setup = {}) { const env: Record<string, string> = { ALLOWED_ORIGINS: `${ORIGIN}, https://www.venusone.co.za`, - Network accessline 32
const env: Record<string, string> = { ALLOWED_ORIGINS: `${ORIGIN}, https://www.venusone.co.za`, RATE_LIMIT_SALT: "test-salt", - Network accessline 33
ALLOWED_ORIGINS: `${ORIGIN}, https://www.venusone.co.za`, - Network accessline 44
}); const fetchFn: Deps["fetch"] = (u, init) => { log.push(u.startsWith("https://challenges.cloudflare.com/") ? "turnstile" : "fetch"); - Network accessline 45
const fetchFn: Deps["fetch"] = (u, init) => { log.push(u.startsWith("https://challenges.cloudflare.com/") ? "turnstile" : "fetch"); return m.fn(u, init); - Network accessline 46
log.push(u.startsWith("https://challenges.cloudflare.com/") ? "turnstile" : "fetch"); - Network accessline 59
function post(body: unknown, headers: Record<string, string> = {}) { return new Request("https://proj.supabase.co/functions/v1/site-check", { - Network accessline 60
return new Request("https://proj.supabase.co/functions/v1/site-check", { - Network accessline 88
Deno.test("cors: missing, unknown and lookalike origins are refused with 403", async () => { for (const origin of [null, "https://evil.example", "https://check.venusone.co.za.evil.example", "null", ORIGIN + "/"]) { - Network accessline 89
for (const origin of [null, "https://evil.example", "https://check.venusone.co.za.evil.example", "null", ORIGIN + "/"]) { - Network accessline 91
const headers: Record<string, string> = { "content-type": "application/json" }; if (origin) headers["origin"] = origin; const { res, body } = await call(s, new Request("https://x.supabase.co/f", { method: "POST", headers, body: "{}" - Network accessline 92
if (origin) headers["origin"] = origin; const { res, body } = await call(s, new Request("https://x.supabase.co/f", { method: "POST", headers, body: "{}" })); assertEquals(res.status, 403, String(origin)); - Network accessline 93
const { res, body } = await call(s, new Request("https://x.supabase.co/f", { method: "POST", headers, body: "{}" })); - Network accessline 100
Deno.test("cors: empty ALLOWED_ORIGINS refuses everything", async () => { const s = setup({ env: { ALLOWED_ORIGINS: "" } }); const { res } = await call(s, post({ url: "https://example.co.za/" })); - Network accessline 101
const s = setup({ env: { ALLOWED_ORIGINS: "" } }); const { res } = await call(s, post({ url: "https://example.co.za/" })); assertEquals(res.status, 403); - Network accessline 102
const { res } = await call(s, post({ url: "https://example.co.za/" })); - Network accessline 106
const s = setup(); const ok = await s.handler( new Request("https://x/f", { method: "OPTIONS", headers: { origin: "https://www.venusone.co.za" } }), - Network accessline 107
const ok = await s.handler( new Request("https://x/f", { method: "OPTIONS", headers: { origin: "https://www.venusone.co.za" } }), ); - Network accessline 108
new Request("https://x/f", { method: "OPTIONS", headers: { origin: "https://www.venusone.co.za" } }), - Network accessline 109
); assertEquals(ok.status, 204); assertEquals(ok.headers.get("access-control-allow-origin"), "https://www.venusone.co.za"); - Network accessline 110
assertEquals(ok.status, 204); assertEquals(ok.headers.get("access-control-allow-origin"), "https://www.venusone.co.za"); assertEquals(ok.headers.get("access-control-allow-methods"), "POST, OPTIONS"); - Network accessline 111
assertEquals(ok.headers.get("access-control-allow-origin"), "https://www.venusone.co.za"); - Network accessline 113
assert(ok.headers.get("access-control-allow-headers")!.includes("content-type")); assertEquals(ok.headers.get("vary"), "Origin"); const bad = await s.handler(new Request("https://x/f", { method: "OPTIONS", headers: { origin: "https://ev - Network accessline 114
assertEquals(ok.headers.get("vary"), "Origin"); const bad = await s.handler(new Request("https://x/f", { method: "OPTIONS", headers: { origin: "https://evil.example" } })); assertEquals(bad.status, 403); - Network accessline 115
const bad = await s.handler(new Request("https://x/f", { method: "OPTIONS", headers: { origin: "https://evil.example" } })); - Network accessline 120
for (const method of ["GET", "PUT", "DELETE"]) { const s = setup(); const { res, body } = await call(s, new Request("https://x/f", { method, headers: { origin: ORIGIN } })); - Network accessline 121
const s = setup(); const { res, body } = await call(s, new Request("https://x/f", { method, headers: { origin: ORIGIN } })); assertEquals(res.status, 405); - Network accessline 122
const { res, body } = await call(s, new Request("https://x/f", { method, headers: { origin: ORIGIN } })); - Network accessline 127
Deno.test("body: JSON only, small, with a url", async () => { const s = setup(); const r1 = await call(s, post({ url: "https://example.co.za/" }, { "content-type": "text/plain" })); - Network accessline 128
const s = setup(); const r1 = await call(s, post({ url: "https://example.co.za/" }, { "content-type": "text/plain" })); assertEquals(r1.res.status, 400); - Network accessline 129
const r1 = await call(s, post({ url: "https://example.co.za/" }, { "content-type": "text/plain" })); - Network accessline 132
const r2 = await call(s, post("{not json")); isError(r2.body, "invalid_request"); const r3 = await call(s, post({ url: "https://example.co.za/" + "a".repeat(9_000) })); - Network accessline 133
isError(r2.body, "invalid_request"); const r3 = await call(s, post({ url: "https://example.co.za/" + "a".repeat(9_000) })); isError(r3.body, "invalid_request"); - Network accessline 134
const r3 = await call(s, post({ url: "https://example.co.za/" + "a".repeat(9_000) })); - Network accessline 136
const r4 = await call(s, post({ url: 42 })); isError(r4.body, "invalid_url"); const r5 = await call(s, post({ url: "https://example.co.za/" + "a".repeat(3_000) })); - Network accessline 137
isError(r4.body, "invalid_url"); const r5 = await call(s, post({ url: "https://example.co.za/" + "a".repeat(3_000) })); isError(r5.body, "invalid_url"); - Network accessline 138
const r5 = await call(s, post({ url: "https://example.co.za/" + "a".repeat(3_000) })); - Network accessline 151
[Credential-related source omitted] - Network accessline 152
[Credential-related source omitted] - Access to secretsline 152
[Credential-related source omitted] - Network accessline 153
const { res, body } = await call(s, post({ url: "https://example.co.za/" })); - Network accessline 160
[Credential-related source omitted] - Network accessline 161
[Credential-related source omitted] - Access to secretsline 161
[Credential-related source omitted] - Network accessline 162
routes: { "https://challenges.cloudflare.com/turnstile/v0/siteverify": turnstile(false, seen) }, - Network accessline 163
}); const r1 = await call(bad, post({ url: "https://example.co.za/", turnstileToken: "tok" })); assertEquals(r1.res.status, 403); - Network accessline 164
const r1 = await call(bad, post({ url: "https://example.co.za/", turnstileToken: "tok" })); - Network accessline 170
[Credential-related source omitted] - Network accessline 171
[Credential-related source omitted] - Access to secretsline 171
[Credential-related source omitted] - Network accessline 172
routes: { "https://challenges.cloudflare.com/turnstile/v0/siteverify": turnstile(true, []) }, - Network accessline 173
}); const r2 = await call(good, post({ url: "https://example.co.za/", turnstileToken: "tok" })); assertEquals(r2.res.status, 200); - Network accessline 174
const r2 = await call(good, post({ url: "https://example.co.za/", turnstileToken: "tok" })); assertEquals(r2.res.status, 200); assertEquals(good.log.slice(0, 4), ["turnstile", "ratelimit", "ratelimit", "fetch"], "per-IP then global, the - Network accessline 179
[Credential-related source omitted] - Network accessline 180
[Credential-related source omitted] - Access to secretsline 180
[Credential-related source omitted] - Network accessline 181
routes: { "https://challenges.cloudflare.com/turnstile/v0/siteverify": () => new Response("oops", { status: 500 }) }, - Network accessline 182
}); const { res } = await call(s, post({ url: "https://example.co.za/", turnstileToken: "tok" })); assertEquals(res.status, 403); - Network accessline 183
const { res } = await call(s, post({ url: "https://example.co.za/", turnstileToken: "tok" })); - Network accessline 186
Deno.test("turnstile: not required when no secret is set", async () => { const s = setup(); const { res } = await call(s, post({ url: "https://example.co.za/" })); - Network accessline 187
const s = setup(); const { res } = await call(s, post({ url: "https://example.co.za/" })); assertEquals(res.status, 200); - Network accessline 188
const { res } = await call(s, post({ url: "https://example.co.za/" })); - Network accessline 202
for (const c of cases) { const s = setup(c); const { res, body } = await call(s, post({ url: "https://example.co.za/" })); - Network accessline 203
const s = setup(c); const { res, body } = await call(s, post({ url: "https://example.co.za/" })); assertEquals(res.status, 429); - Network accessline 204
const { res, body } = await call(s, post({ url: "https://example.co.za/" })); - Network accessline 210
Deno.test("rate limit: salted hash of the client IP, default and custom limits", async () => { const s = setup(); await call(s, post({ url: "https://example.co.za/" })); - Network accessline 211
const s = setup(); await call(s, post({ url: "https://example.co.za/" })); assertEquals(s.hits[0]!.limit, 10); - Network accessline 212
await call(s, post({ url: "https://example.co.za/" })); - Network accessline 216
assert(!s.hits[0]!.hash.includes("41.1.2.3")); const s2 = setup({ env: { RATE_LIMIT_PER_HOUR: "3" } }); await call(s2, post({ url: "https://example.co.za/" })); - Network accessline 217
const s2 = setup({ env: { RATE_LIMIT_PER_HOUR: "3" } }); await call(s2, post({ url: "https://example.co.za/" })); assertEquals(s2.hits[0]!.limit, 3); - Network accessline 218
await call(s2, post({ url: "https://example.co.za/" })); - Network accessline 219
assertEquals(s2.hits[0]!.limit, 3); const s3 = setup({ env: { RATE_LIMIT_PER_HOUR: "-5" } }); await call(s3, post({ url: "https://example.co.za/" })); - Network accessline 220
const s3 = setup({ env: { RATE_LIMIT_PER_HOUR: "-5" } }); await call(s3, post({ url: "https://example.co.za/" })); assertEquals(s3.hits[0]!.limit, 10); - Network accessline 221
await call(s3, post({ url: "https://example.co.za/" })); - Network accessline 225
Deno.test("rate limit: rate limiting happens before URL validation", async () => { const s = setup(); await call(s, post({ url: "http://127.0.0.1/" })); - Network accessline 226
const s = setup(); await call(s, post({ url: "http://127.0.0.1/" })); assertEquals(s.hits.length, 2, "per-IP and global checks both run before URL validation"); - Network accessline 227
await call(s, post({ url: "http://127.0.0.1/" })); - Network accessline 251
try { const env = (k: string) => ({ SUPABASE_URL: "https://proj.supabase.co", SUPABASE_SERVICE_ROLE_KEY: "srk" } as Record<string, string>)[k]; - Network accessline 252
const env = (k: string) => ({ SUPABASE_URL: "https://proj.supabase.co", SUPABASE_SERVICE_ROLE_KEY: "srk" } as Record<string, string>)[k]; const sig = new AbortController().signal; - Network accessline 253
({ SUPABASE_URL: "https://proj.supabase.co", SUPABASE_SERVICE_ROLE_KEY: "srk" } as Record<string, string>)[k]; - Network accessline 254
const sig = new AbortController().signal; assertEquals(await supabaseRateLimit("a".repeat(64), 7, sig, env), true); assertEquals(seen[0]!.url, "https://proj.supabase.co/rest/v1/rpc/site_check_hit"); - Network accessline 255
assertEquals(await supabaseRateLimit("a".repeat(64), 7, sig, env), true); assertEquals(seen[0]!.url, "https://proj.supabase.co/rest/v1/rpc/site_check_hit"); assertEquals(JSON.parse(seen[0]!.body), { p_ip_hash: "a".repeat(64), p_ - Network accessline 256
assertEquals(seen[0]!.url, "https://proj.supabase.co/rest/v1/rpc/site_check_hit"); - Network accessline 286
[Credential-related source omitted] - Network accessline 287
[Credential-related source omitted] - Network accessline 288
[Credential-related source omitted] - Network accessline 289
`http://169.254.169.254/latest/meta-data/?k=${SECRET}`, - Network accessline 290
`http://localhost/${SECRET}`, - Network accessline 291
`ftp://example.co.za/${SECRET}`, `http://example.co.za:8080/${SECRET}`, ] - Network accessline 292
`http://example.co.za:8080/${SECRET}`, - Network accessline 304
[Credential-related source omitted] - Network accessline 305
[Credential-related source omitted] - Network accessline 306
[Credential-related source omitted] - Network accessline 307
[Credential-related source omitted] - Network accessline 309
[Credential-related source omitted] - Network accessline 310
[Credential-related source omitted] - Network accessline 311
[Credential-related source omitted] - Network accessline 316
Deno.test("errors: DNS failure and redirect loops", async () => { const s = setup(); const r1 = await call(s, post({ url: "https://unknown.example.co.za/" })); - Network accessline 317
const s = setup(); const r1 = await call(s, post({ url: "https://unknown.example.co.za/" })); assertEquals(r1.res.status, 502); - Network accessline 318
const r1 = await call(s, post({ url: "https://unknown.example.co.za/" })); - Network accessline 320
isError(r1.body, "dns_failed"); const s2 = setup({ routes: { "https://example.co.za/": () => new Response(null, { status: 302, headers: { location: "/" } }) }, - Network accessline 321
const s2 = setup({ routes: { "https://example.co.za/": () => new Response(null, { status: 302, headers: { location: "/" } }) }, }); - Network accessline 322
routes: { "https://example.co.za/": () => new Response(null, { status: 302, headers: { location: "/" } }) }, - Network accessline 323
}); const r2 = await call(s2, post({ url: "https://example.co.za/" })); isError(r2.body, "too_many_redirects"); - Network accessline 324
const r2 = await call(s2, post({ url: "https://example.co.za/" })); - Network accessline 334
]; for (const [route, type] of cases) { const s = setup({ routes: { "https://example.co.za/": route } }); - Network accessline 335
for (const [route, type] of cases) { const s = setup({ routes: { "https://example.co.za/": route } }); const { res, body } = await call(s, post({ url: "https://example.co.za/" })); - Network accessline 336
const s = setup({ routes: { "https://example.co.za/": route } }); - Network accessline 337
const { res, body } = await call(s, post({ url: "https://example.co.za/" })); - Network accessline 342
Deno.test("success: response shape, headers and a clean site", async () => { const s = setup(); const { res, body } = await call(s, post({ url: "https://user:pw@example.co.za/#x" })); - Network accessline 343
const s = setup(); const { res, body } = await call(s, post({ url: "https://user:pw@example.co.za/#x" })); assertEquals(res.status, 200); - Network accessline 344
const { res, body } = await call(s, post({ url: "https://user:pw@example.co.za/#x" })); - Network accessline 348
assertEquals(res.headers.get("content-type"), "application/json; charset=utf-8"); assertEquals(Object.keys(body), ["url", "final_url", "checked_at", "summary", "findings", "cookies", "notices", "disclaimer"]); assertEquals(body["url"], - Network accessline 349
assertEquals(Object.keys(body), ["url", "final_url", "checked_at", "summary", "findings", "cookies", "notices", "disclaimer"]); assertEquals(body["url"], "https://example.co.za/"); assertEquals(body["checked_at"], "2026-10-18T09:00:00+0 - Network accessline 350
assertEquals(body["url"], "https://example.co.za/"); - Network accessline 361
const shell = `<html><body><div id="root"></div></body></html>`; const routes: Record<string, Route> = { "https://example.co.za/": html(shell), - Network accessline 362
const routes: Record<string, Route> = { "https://example.co.za/": html(shell), "https://example.co.za/privacy": html(shell), // SPA fallback: same body, not a notice - Network accessline 363
"https://example.co.za/": html(shell), - Network accessline 364
"https://example.co.za/privacy": html(shell), // SPA fallback: same body, not a notice - Network accessline 365
"https://example.co.za/privacy-policy": html("<h1>Privacy notice</h1>"), - Network accessline 366
"https://example.co.za/paia-manual": () => new Response("%PDF-1.7", { headers: { "content-type": "application/pdf" } }), - Network accessline 367
}; const off = setup({ routes }); const r1 = await call(off, post({ url: "https://example.co.za/" })); - Network accessline 368
const off = setup({ routes }); const r1 = await call(off, post({ url: "https://example.co.za/" })); assertEquals(r1.body["notices"], { privacy: false, paia: false, cookie: false, information_officer: false }); - Network accessline 369
const r1 = await call(off, post({ url: "https://example.co.za/" })); - Network accessline 371
assert(!off.calls.some((c) => c.url.endsWith("/privacy"))); const on = setup({ routes, env: { PROBE_NOTICE_PATHS: "true" } }); const r2 = await call(on, post({ url: "https://example.co.za/" })); - Network accessline 372
const on = setup({ routes, env: { PROBE_NOTICE_PATHS: "true" } }); const r2 = await call(on, post({ url: "https://example.co.za/" })); assertEquals(r2.body["notices"], { privacy: true, paia: true, cookie: false, information_officer: fal - Network accessline 373
const r2 = await call(on, post({ url: "https://example.co.za/" })); - Network accessline 379
Deno.test("copy: no dashes, exclamation marks or compliance claims in user-facing strings", async () => { const strings: string[] = [...Object.values(ERROR_MESSAGES), DISCLAIMER]; const bad = `<script src="http://www.googletagmanager.co - Network accessline 380
const strings: string[] = [...Object.values(ERROR_MESSAGES), DISCLAIMER]; const bad = `<script src="http://www.googletagmanager.com/gtag/js"></script><script>fbq('init','1')</script> <form><input type="password"><input type="checkbox" - Network accessline 381
const bad = `<script src="http://www.googletagmanager.com/gtag/js"></script><script>fbq('init','1')</script> - Network accessline 388
h.append("set-cookie", "_ga=1"); h.append("set-cookie", "sessionid=1"); const s = setup({ routes: { "http://example.co.za/": () => new Response(bad, { headers: h }) } }); - Network accessline 389
h.append("set-cookie", "sessionid=1"); const s = setup({ routes: { "http://example.co.za/": () => new Response(bad, { headers: h }) } }); const { body } = await call(s, post({ url: "http://example.co.za/" })); - Network accessline 390
const s = setup({ routes: { "http://example.co.za/": () => new Response(bad, { headers: h }) } }); - Network accessline 391
const { body } = await call(s, post({ url: "http://example.co.za/" }));
hosted/site-check/tests/hardening.test.ts
- Executable scriptline 1
Executable file - Network accessline 34
const ORIGIN = "https://check.venusone.co.za"; - Network accessline 38
const env: Record<string, string> = { ALLOWED_ORIGINS: ORIGIN, RATE_LIMIT_SALT: SALT, ...o.env }; const m = mockFetch({ "https://example.co.za/": html("<p>ok</p>"), - Network accessline 39
const m = mockFetch({ "https://example.co.za/": html("<p>ok</p>"), }); - Network accessline 40
"https://example.co.za/": html("<p>ok</p>"), - Network accessline 56
function post(headers: Record<string, string> = {}, body: BodyInit = JSON.stringify({ url: "https://example.co.za/" })) { - Network accessline 57
return new Request("https://proj.supabase.co/functions/v1/site-check", { - Network accessline 72
const noEnv = () => undefined; const reqWith = (h: Record<string, string>) => new Request("https://x/f", { headers: h }); - Network accessline 73
const reqWith = (h: Record<string, string>) => new Request("https://x/f", { headers: h }); - Network accessline 85
h.append("x-forwarded-for", "6.6.6.6"); h.append("x-forwarded-for", "41.0.0.3"); assertEquals(clientIp(new Request("https://x/f", { headers: h }), noEnv), "41.0.0.3", "repeated header lines"); - Network accessline 86
h.append("x-forwarded-for", "41.0.0.3"); assertEquals(clientIp(new Request("https://x/f", { headers: h }), noEnv), "41.0.0.3", "repeated header lines"); assertEquals(clientIp(reqWith({}), noEnv), null); - Network accessline 87
assertEquals(clientIp(new Request("https://x/f", { headers: h }), noEnv), "41.0.0.3", "repeated header lines"); - Network accessline 182
Deno.test("redos: safeUrl bounds long paths and never leaves part of a token at the cut", () => { let t = performance.now(); const out = safeUrl("https://e.co.za/" + "a".repeat(1_000_000)); - Network accessline 183
let t = performance.now(); const out = safeUrl("https://e.co.za/" + "a".repeat(1_000_000)); fast(performance.now() - t, 200, "1 MB path"); - Network accessline 184
const out = safeUrl("https://e.co.za/" + "a".repeat(1_000_000)); - Network accessline 186
assertEquals(out, "https://e.co.za/…"); - Network accessline 188
const path = "/" + "p/".repeat(245) + "Q".repeat(40) + "/tail"; t = performance.now(); const cut = safeUrl("https://e.co.za" + path); - Network accessline 189
t = performance.now(); const cut = safeUrl("https://e.co.za" + path); fast(performance.now() - t, 50, "cut path"); - Network accessline 190
const cut = safeUrl("https://e.co.za" + path); - Network accessline 192
assert(!cut.includes("QQQQ"), cut.slice(-40)); assert(cut.endsWith("/…"), cut.slice(-10)); assert(cut.length <= "https://e.co.za".length + LIMITS.maxReportPath + 1); - Network accessline 193
assert(cut.endsWith("/…"), cut.slice(-10)); assert(cut.length <= "https://e.co.za".length + LIMITS.maxReportPath + 1); const mail = safeUrl("https://e.co.za/" + "x/".repeat(250) + "person@example.test/" + "y".repeat(600)); - Network accessline 194
assert(cut.length <= "https://e.co.za".length + LIMITS.maxReportPath + 1); - Network accessline 195
const mail = safeUrl("https://e.co.za/" + "x/".repeat(250) + "person@example.test/" + "y".repeat(600)); - Network accessline 196
assert(!mail.includes("person"), "email near the cut is still masked"); assertEquals(safeUrl("https://e.co.za/k/" + "a".repeat(40)), "https://e.co.za/k/…REDACTED", "short paths unchanged"); }); - Network accessline 197
assertEquals(safeUrl("https://e.co.za/k/" + "a".repeat(40)), "https://e.co.za/k/…REDACTED", "short paths unchanged"); - Network accessline 199
Deno.test("redos: one huge http script src, or thousands of them, stays fast", () => { let t = performance.now(); const one = checkHtml(`<script src="http://cdn.example.co.za/${"a".repeat(1_400_000)}"></script>`, "https://e.co.za/"); - Network accessline 200
let t = performance.now(); const one = checkHtml(`<script src="http://cdn.example.co.za/${"a".repeat(1_400_000)}"></script>`, "https://e.co.za/"); fast(performance.now() - t, 1_000, "1.4 MB src"); - Network accessline 201
const one = checkHtml(`<script src="http://cdn.example.co.za/${"a".repeat(1_400_000)}"></script>`, "https://e.co.za/"); - Network accessline 204
assertEquals(w12.length, 1); assert(w12[0]!.evidence.length <= LIMITS.maxEvidence); const many = `<script src="http://cdn.example.co.za/${"b".repeat(400)}"></script>`.repeat(3_000); - Network accessline 205
assert(w12[0]!.evidence.length <= LIMITS.maxEvidence); const many = `<script src="http://cdn.example.co.za/${"b".repeat(400)}"></script>`.repeat(3_000); t = performance.now(); - Network accessline 206
const many = `<script src="http://cdn.example.co.za/${"b".repeat(400)}"></script>`.repeat(3_000); - Network accessline 207
t = performance.now(); const r = checkHtml(many, "https://e.co.za/"); fast(performance.now() - t, 1_500, "3000 http scripts"); - Network accessline 208
const r = checkHtml(many, "https://e.co.za/"); - Network accessline 213
const t = performance.now(); checkHeaders({ finalUrl: "https://e.co.za/", - Network accessline 214
checkHeaders({ finalUrl: "https://e.co.za/", headers: new Headers({ server: "1".repeat(400_000), "x-powered-by": "9".repeat(400_000) }), - Network accessline 215
finalUrl: "https://e.co.za/", - Network accessline 217
}); fast(performance.now() - t, 100, "400 KB digit headers"); const v = checkHeaders({ finalUrl: "https://e.co.za/", headers: new Headers({ server: "nginx/1.25.3" }) }); - Network accessline 218
fast(performance.now() - t, 100, "400 KB digit headers"); const v = checkHeaders({ finalUrl: "https://e.co.za/", headers: new Headers({ server: "nginx/1.25.3" }) }); assert(v.some((f) => f.rule_id === "WEB-009" && f.evidence === "nginx/ - Network accessline 219
const v = checkHeaders({ finalUrl: "https://e.co.za/", headers: new Headers({ server: "nginx/1.25.3" }) }); - Network accessline 223
const long = "/" + "c".repeat(300_000); const m = mockFetch({ "https://example.co.za/": () => new Response(null, { status: 302, headers: { location: long } }), - Network accessline 224
const m = mockFetch({ "https://example.co.za/": () => new Response(null, { status: 302, headers: { location: long } }), ["https://example.co.za" + long]: html("<p>ok</p>"), - Network accessline 225
"https://example.co.za/": () => new Response(null, { status: 302, headers: { location: long } }), - Network accessline 226
["https://example.co.za" + long]: html("<p>ok</p>"), - Network accessline 236
fast(performance.now() - t, 1_000, "long final URL"); const body = await res.json(); assertEquals(body.final_url, "https://example.co.za/…"); - Network accessline 237
const body = await res.json(); assertEquals(body.final_url, "https://example.co.za/…"); }); - Network accessline 238
assertEquals(body.final_url, "https://example.co.za/…"); - Network accessline 301
let name = ""; try { await guardedFetch("https://example.co.za/", ctx, { follow: true, bodyLimit: () => LIMITS.maxBodyBytes }); - Network accessline 302
try { await guardedFetch("https://example.co.za/", ctx, { follow: true, bodyLimit: () => LIMITS.maxBodyBytes }); } catch (e) { - Network accessline 303
await guardedFetch("https://example.co.za/", ctx, { follow: true, bodyLimit: () => LIMITS.maxBodyBytes }); - Network accessline 366
for ( const u of [ "http://127%2e0%2e0%2e1/", - Network accessline 367
const u of [ "http://127%2e0%2e0%2e1/", "http://127.0.0.1./", - Network accessline 368
"http://127%2e0%2e0%2e1/", - Network accessline 369
"http://127.0.0.1./", - Network accessline 370
"http://017700000001/", - Network accessline 371
"http://0x7f.0x0.0x0.0x1/", - Network accessline 372
"http://0.0.0.0./", - Network accessline 373
"http://[::]/", - Network accessline 374
"http://[::127.0.0.1]/", - Network accessline 375
"http://[0:0:0:0:0:ffff:127.0.0.1]/", - Network accessline 376
"http://[64:ff9b::169.254.169.254]/", - Network accessline 377
"http://[64:ff9b::a00:1]/", - Network accessline 378
"http://[2002:a9fe:a9fe::1]/", - Network accessline 379
"http://[2002:7f00:1::]/", - Network accessline 380
"http://[2001:0:4136:e378:8000:63bf:3fff:fdd2]/", - Network accessline 381
"http://[fec0::1]/", - Network accessline 382
"http://[fd00:ec2::254]/", - Network accessline 383
"http://127.0.0.1/", - Network accessline 384
"http://127。0。0。1/", - Network accessline 385
"http://ⓛⓞⓒⓐⓛⓗⓞⓢⓣ/", - Network accessline 386
"http://localhost%2e/", - Network accessline 387
"http://example.co.za\n@127.0.0.1/", - Network accessline 388
"https:////127.0.0.1/", - Network accessline 389
"http://168.63.129.16/", - Network accessline 390
"http://100.100.100.200/", - Network accessline 391
"http://metadata.google.internal./", - Network accessline 436
Deno.test("workers: every outbound fetch uses redirect manual, so a full check passes on Workers", async () => { const modes: string[] = []; const doh = (n: string, t: string) => `https://cloudflare-dns.com/dns-query?name=${n}&type=${t} - Network accessline 437
const modes: string[] = []; const doh = (n: string, t: string) => `https://cloudflare-dns.com/dns-query?name=${n}&type=${t}`; const json = (v: unknown) => () => new Response(JSON.stringify(v), { headers: { "content-type": "application/j - Network accessline 438
const doh = (n: string, t: string) => `https://cloudflare-dns.com/dns-query?name=${n}&type=${t}`; - Network accessline 441
[doh("example.co.za", "A")]: json({ Status: 0, Answer: [{ type: 1, data: "203.0.114.1" }] }), [doh("example.co.za", "AAAA")]: json({ Status: 0 }), "https://challenges.cloudflare.com/turnstile/v0/siteverify": json({ success: true }), - Network accessline 442
[doh("example.co.za", "AAAA")]: json({ Status: 0 }), "https://challenges.cloudflare.com/turnstile/v0/siteverify": json({ success: true }), "https://example.co.za/": () => - Network accessline 443
"https://challenges.cloudflare.com/turnstile/v0/siteverify": json({ success: true }), - Network accessline 444
"https://example.co.za/": () => - Network accessline 457
[Credential-related source omitted] - Network accessline 458
[Credential-related source omitted] - Network accessline 459
[Credential-related source omitted] - Network accessline 463
}); const res = await handler( new Request("https://site.example.co.za/api/site-check", { - Network accessline 464
const res = await handler( new Request("https://site.example.co.za/api/site-check", { method: "POST", - Network accessline 465
new Request("https://site.example.co.za/api/site-check", { - Network accessline 466
method: "POST", headers: { origin: "https://site.example.co.za", "content-type": "application/json" }, body: JSON.stringify({ url: "example.co.za", turnstileToken: "tok" }), - Network accessline 467
headers: { origin: "https://site.example.co.za", "content-type": "application/json" },
hosted/site-check/tests/helpers.ts
- Executable scriptline 1
Executable file
hosted/site-check/tests/sql/run_sql_tests.sh
i18n/README.md
- Network accessline 55
1. Fork https://github.com/VenusOne-Lee/popia-compliance and create a branch, for example `i18n-zu-review`.
mcp/__init__.py
- Executable scriptline 1
Executable file
mcp/server.py
- Executable scriptline 1
#!/usr/bin/env python3 - Access to secretsline 257
[Credential-related source omitted] - Access to secretsline 372
[Credential-related source omitted] - Access to secretsline 373
[Credential-related source omitted] - Access to secretsline 378
[Credential-related source omitted] - Access to secretsline 474
[Credential-related source omitted]
mcp/smoke_test.py
- Executable scriptline 1
#!/usr/bin/env python3
package.json
- Executable scriptline 1
Executable file - Network accessline 5
"license": "MIT", "author": "Venus ARC (Pty) Ltd, trading as Venus One", "homepage": "https://github.com/VenusOne-Lee/popia-compliance", - Network accessline 6
"author": "Venus ARC (Pty) Ltd, trading as Venus One", "homepage": "https://github.com/VenusOne-Lee/popia-compliance", "repository": {"type": "git", "url": "git+https://github.com/VenusOne-Lee/popia-compliance.git"}, - Network accessline 7
"homepage": "https://github.com/VenusOne-Lee/popia-compliance", - Network accessline 8
"repository": {"type": "git", "url": "git+https://github.com/VenusOne-Lee/popia-compliance.git"},
packs/README.md
- Network accessline 52
1. **Open an issue first** at https://github.com/VenusOne-Lee/popia-compliance/issues with the sector, the main laws and who will maintain the pack.
pyproject.toml
- Network accessline 34
[project.urls] Homepage = "https://popiakit.co.za" - Network accessline 35
Homepage = "https://popiakit.co.za" - Network accessline 36
"Site check" = "https://popiakit.co.za/check" - Network accessline 37
Repository = "https://github.com/VenusOne-Lee/popia-compliance" - Network accessline 38
Documentation = "https://github.com/VenusOne-Lee/popia-compliance/blob/main/docs/quickstart.md" - Network accessline 39
Changelog = "https://github.com/VenusOne-Lee/popia-compliance/blob/main/CHANGELOG.md" - Network accessline 40
Security = "https://github.com/VenusOne-Lee/popia-compliance/blob/main/SECURITY.md" - Network accessline 41
Issues = "https://github.com/VenusOne-Lee/popia-compliance/issues"
references/02-build-patterns.md
- Network accessline 129
function loadScript(src) { const s = document.createElement('script'); s.src = src; s.async = true; document.head.appendChild(s); } function applyConsent(c) { if (c?.analytics) loadScript('https://plausible.io/js/script.js'); - Network accessline 130
function applyConsent(c) { if (c?.analytics) loadScript('https://plausible.io/js/script.js'); // or GA4 with consent mode if (c?.marketing) { /* load Meta pixel, LinkedIn Insight, TikTok here, never before */ } - Network accessline 131
if (c?.analytics) loadScript('https://plausible.io/js/script.js'); // or GA4 with consent mode
references/04-stack-playbooks.md
references/07-breach-response.md
- Access to secretsline 65
[Credential-related source omitted]
references/09-audit-procedure.md
- Network accessline 62
python3 scripts/popia_scan.py ./repo --format json --out evidence/popia-scan.json # live site (passive) python3 scripts/site_check.py https://example.co.za --out evidence/site-check.md --json evidence/site-check.json - Network accessline 63
# live site (passive) python3 scripts/site_check.py https://example.co.za --out evidence/site-check.md --json evidence/site-check.json # extra tools if available - Network accessline 64
python3 scripts/site_check.py https://example.co.za --out evidence/site-check.md --json evidence/site-check.json - Network accessline 196
Severity: Critical Title: audit_leads table readable by anonymous users Evidence: migration 20260712_create_leads.sql:4 creates table without RLS; curl with anon key returned 1,204 rows (redacted sample in evidence/F-07.txt) - Network accessline 197
Title: audit_leads table readable by anonymous users Evidence: migration 20260712_create_leads.sql:4 creates table without RLS; curl with anon key returned 1,204 rows (redacted sample in evidence/F-07.txt) Risk: Any visitor can - Network accessline 198
Evidence: migration 20260712_create_leads.sql:4 creates table without RLS; curl with anon key returned 1,204 rows (redacted sample in evidence/F-07.txt)
registry/README.md
- Network accessline 41
Base URL: `https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r` - Network accessline 44
# Consent system (provider, banner, gate, record helpers) npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-system.json - Network accessline 45
npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-system.json - Network accessline 47
# Forms npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/marketing-opt-in.json - Network accessline 48
npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/marketing-opt-in.json - Network accessline 49
npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/collection-notice.json - Network accessline 50
npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/sa-id-input.json - Network accessline 52
# Libraries npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/redact.json - Network accessline 53
npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/redact.json - Network accessline 54
npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/sa-id.json - Network accessline 56
# Account npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/privacy-centre.json - Network accessline 57
npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/privacy-centre.json - Network accessline 59
# Supabase migrations and edge functions npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/supabase-privacy-schema.json - Network accessline 60
npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/supabase-privacy-schema.json - Network accessline 61
npx shadcn@latest add https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/supabase-privacy-functions.json - Network accessline 116
<App /> <GoogleConsentModeSync /> <ConsentGate category="analytics" src="https://plausible.io/js/script.js" scriptOptions={{ attributes: { "data-domain": "example.co.za" } }} /> - Network accessline 117
<GoogleConsentModeSync /> <ConsentGate category="analytics" src="https://plausible.io/js/script.js" scriptOptions={{ attributes: { "data-domain": "example.co.za" } }} /> <ConsentBanner companyName="Example (Pty) Ltd" privacyNoticeUrl="/ - Network accessline 118
<ConsentGate category="analytics" src="https://plausible.io/js/script.js" scriptOptions={{ attributes: { "data-domain": "example.co.za" } }} /> - Access to secretsline 156
[Credential-related source omitted] - Access to secretsline 157
[Credential-related source omitted] - Network accessline 183
Add POPIA (South Africa) privacy features to this app using the open-source registry at https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/ - Network accessline 184
https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/ - Network accessline 249
supabase secrets set \ ALLOWED_ORIGINS="https://example.co.za,https://www.example.co.za" \ - Network accessline 250
ALLOWED_ORIGINS="https://example.co.za,https://www.example.co.za" \
registry/build.py
- Executable scriptline 1
#!/usr/bin/env python3 - Network accessline 23
SOURCE = HERE / "registry.json" OUT = HERE / "r" ITEM_SCHEMA = "https://ui.shadcn.com/schema/registry-item.json" - Network accessline 24
OUT = HERE / "r" ITEM_SCHEMA = "https://ui.shadcn.com/schema/registry-item.json" REGISTRY_SCHEMA = "https://ui.shadcn.com/schema/registry.json" - Network accessline 25
ITEM_SCHEMA = "https://ui.shadcn.com/schema/registry-item.json" - Network accessline 26
REGISTRY_SCHEMA = "https://ui.shadcn.com/schema/registry.json"
registry/popia/account/privacy-centre.tsx
- Executable scriptline 1
Executable file - Access to secretsline 51
[Credential-related source omitted] - Network accessline 94
objectionFormUrl, privacyContact, regulatorComplaintUrl = "https://inforegulator.org.za/", // VERIFY current complaints page - Network accessline 95
privacyContact, regulatorComplaintUrl = "https://inforegulator.org.za/", // VERIFY current complaints page retainedDataNote = "We keep some records after you delete your account where the law requires it, for example invoices for tax pu - Network accessline 96
regulatorComplaintUrl = "https://inforegulator.org.za/", // VERIFY current complaints page - Network accessline 108
[Credential-related source omitted] - Network accessline 109
if (!token) throw new Error("You need to be signed in."); const res = await fetch(url(path), { ...init, - Network accessline 110
const res = await fetch(url(path), {
registry/popia/consent/consent-banner.tsx
- Executable scriptline 1
Executable file
registry/popia/consent/consent-gate.tsx
- Executable scriptline 1
Executable file - Network accessline 11
* * Usage: * <ConsentGate category="marketing" src="https://connect.facebook.net/en_US/fbevents.js" /> - Network accessline 12
* Usage: * <ConsentGate category="marketing" src="https://connect.facebook.net/en_US/fbevents.js" /> * <ConsentGate category="analytics" fallback={<p>Enable analytics cookies to see this.</p>}> - Network accessline 13
* <ConsentGate category="marketing" src="https://connect.facebook.net/en_US/fbevents.js" />
registry/popia/consent/consent-provider.tsx
- Executable scriptline 1
Executable file
registry/popia/forms/collection-notice.tsx
- Executable scriptline 1
Executable file
registry/popia/forms/marketing-opt-in.tsx
- Executable scriptline 1
Executable file
registry/popia/forms/sa-id-input.tsx
- Executable scriptline 1
Executable file
registry/popia/lib/consent-record.ts
- Executable scriptline 1
Executable file
registry/popia/lib/redact.ts
registry/popia/lib/sa-id.ts
- Executable scriptline 1
Executable file
registry/popia/supabase/functions/_shared/privacy-config.ts
registry/popia/supabase/functions/consent/index.ts
- Executable scriptline 1
Executable file
registry/popia/supabase/functions/dsar-delete/index.ts
- Executable scriptline 1
Executable file
registry/popia/supabase/functions/dsar-export/index.ts
- Executable scriptline 1
Executable file
registry/popia/supabase/functions/unsubscribe/index.ts
registry/r/collection-notice.json
registry/r/consent-banner.json
- Network accessline 1
{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 2
"$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 11
"label", "switch", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 12
"switch", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" ], - Network accessline 13
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json"
registry/r/consent-gate.json
- Network accessline 1
{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 2
"$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 7
"dependencies": [], "registryDependencies": [ "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 8
"registryDependencies": [ "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" ], - Network accessline 9
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 12
{ "path": "popia/consent/consent-gate.tsx", "content": "\"use client\";\n\n/**\n * ConsentGate, loadScript and Google Consent Mode v2 helpers.\n *\n * Nothing in the analytics or marketing category may load before the person\n * - Network accessline 13
"path": "popia/consent/consent-gate.tsx", "content": "\"use client\";\n\n/**\n * ConsentGate, loadScript and Google Consent Mode v2 helpers.\n *\n * Nothing in the analytics or marketing category may load before the person\n * opts in - Network accessline 14
"content": "\"use client\";\n\n/**\n * ConsentGate, loadScript and Google Consent Mode v2 helpers.\n *\n * Nothing in the analytics or marketing category may load before the person\n * opts in. That covers pixels, tag managers, session repl
registry/r/consent-provider.json
registry/r/consent-record.json
registry/r/consent-system.json
- Network accessline 1
{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 2
"$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 27
{ "path": "popia/consent/consent-gate.tsx", "content": "\"use client\";\n\n/**\n * ConsentGate, loadScript and Google Consent Mode v2 helpers.\n *\n * Nothing in the analytics or marketing category may load before the person\n * - Network accessline 28
"path": "popia/consent/consent-gate.tsx", "content": "\"use client\";\n\n/**\n * ConsentGate, loadScript and Google Consent Mode v2 helpers.\n *\n * Nothing in the analytics or marketing category may load before the person\n * opts in - Network accessline 29
"content": "\"use client\";\n\n/**\n * ConsentGate, loadScript and Google Consent Mode v2 helpers.\n *\n * Nothing in the analytics or marketing category may load before the person\n * opts in. That covers pixels, tag managers, session repl
registry/r/marketing-opt-in.json
- Network accessline 1
{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 2
"$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 9
"checkbox", "label", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-record.json" - Network accessline 10
"label", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-record.json" ], - Network accessline 11
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-record.json"
registry/r/privacy-centre.json
- Network accessline 1
{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 2
"$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 12
"label", "switch", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 13
"switch", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" ], - Network accessline 14
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 17
{ "path": "popia/account/privacy-centre.tsx", "content": "\"use client\";\n\n/**\n * Privacy centre for signed-in users.\n *\n * Builds the data subject rights into the product (references/02-build-patterns.md, section 10):\n * - Network accessline 18
"path": "popia/account/privacy-centre.tsx", "content": "\"use client\";\n\n/**\n * Privacy centre for signed-in users.\n *\n * Builds the data subject rights into the product (references/02-build-patterns.md, section 10):\n * - Down - Network accessline 19
[Credential-related source omitted] - Access to secretsline 19
[Credential-related source omitted]
registry/r/redact.json
registry/r/registry.json
- Network accessline 1
{ "$schema": "https://ui.shadcn.com/schema/registry.json", - Network accessline 2
"$schema": "https://ui.shadcn.com/schema/registry.json", - Network accessline 3
"name": "popia", "homepage": "https://github.com/VenusOne-Lee/popia-compliance", "items": [ - Network accessline 4
"homepage": "https://github.com/VenusOne-Lee/popia-compliance", - Network accessline 30
"label", "switch", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 31
"switch", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" ], - Network accessline 32
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 47
"dependencies": [], "registryDependencies": [ "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 48
"registryDependencies": [ "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" ], - Network accessline 49
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 146
"checkbox", "label", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-record.json" - Network accessline 147
"label", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-record.json" ], - Network accessline 148
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-record.json" - Network accessline 180
"input", "label", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/sa-id.json" - Network accessline 181
"label", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/sa-id.json" ], - Network accessline 182
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/sa-id.json" - Network accessline 202
"label", "switch", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 203
"switch", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" ], - Network accessline 204
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 249
"dependencies": [], "registryDependencies": [ "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/supabase-privacy-schema.json" - Network accessline 250
"registryDependencies": [ "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/supabase-privacy-schema.json" ], - Network accessline 251
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/supabase-privacy-schema.json"
registry/r/sa-id-input.json
- Network accessline 1
{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 2
"$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 9
"input", "label", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/sa-id.json" - Network accessline 10
"label", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/sa-id.json" ], - Network accessline 11
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/sa-id.json"
registry/r/sa-id.json
registry/r/supabase-privacy-functions.json
- Network accessline 1
{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 2
"$schema": "https://ui.shadcn.com/schema/registry-item.json", - Network accessline 7
"dependencies": [], "registryDependencies": [ "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/supabase-privacy-schema.json" - Network accessline 8
"registryDependencies": [ "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/supabase-privacy-schema.json" ], - Network accessline 9
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/supabase-privacy-schema.json" - Access to secretsline 14
[Credential-related source omitted] - Access to secretsline 38
[Credential-related source omitted]
registry/r/supabase-privacy-schema.json
registry/registry.json
- Network accessline 1
{ "$schema": "https://ui.shadcn.com/schema/registry.json", - Network accessline 2
"$schema": "https://ui.shadcn.com/schema/registry.json", - Network accessline 3
"name": "popia", "homepage": "https://github.com/VenusOne-Lee/popia-compliance", "items": [ - Network accessline 4
"homepage": "https://github.com/VenusOne-Lee/popia-compliance", - Network accessline 30
"label", "switch", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 31
"switch", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" ], - Network accessline 32
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 47
"dependencies": [], "registryDependencies": [ "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 48
"registryDependencies": [ "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" ], - Network accessline 49
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 146
"checkbox", "label", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-record.json" - Network accessline 147
"label", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-record.json" ], - Network accessline 148
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-record.json" - Network accessline 180
"input", "label", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/sa-id.json" - Network accessline 181
"label", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/sa-id.json" ], - Network accessline 182
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/sa-id.json" - Network accessline 202
"label", "switch", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 203
"switch", "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" ], - Network accessline 204
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/consent-provider.json" - Network accessline 249
"dependencies": [], "registryDependencies": [ "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/supabase-privacy-schema.json" - Network accessline 250
"registryDependencies": [ "https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/supabase-privacy-schema.json" ], - Network accessline 251
"https://raw.githubusercontent.com/VenusOne-Lee/popia-compliance/main/registry/r/supabase-privacy-schema.json"
regwatch/sources.json
- Network accessline 1
{ "user_agent": "popia-compliance-regwatch/1.0 (+https://github.com/VenusOne-Lee/popia-compliance; monthly check)", - Network accessline 2
"user_agent": "popia-compliance-regwatch/1.0 (+https://github.com/VenusOne-Lee/popia-compliance; monthly check)", - Network accessline 7
"id": "media-statements", "name": "Information Regulator media statements", "url": "https://inforegulator.org.za/media-statements/", - Network accessline 8
"name": "Information Regulator media statements", "url": "https://inforegulator.org.za/media-statements/", "enabled": true - Network accessline 9
"url": "https://inforegulator.org.za/media-statements/", - Network accessline 13
"id": "enforcement-notices", "name": "Information Regulator enforcement notices", "url": "https://inforegulator.org.za/enforcement-notices/", - Network accessline 14
"name": "Information Regulator enforcement notices", "url": "https://inforegulator.org.za/enforcement-notices/", "enabled": true - Network accessline 15
"url": "https://inforegulator.org.za/enforcement-notices/", - Network accessline 19
"id": "guidance-notes", "name": "Information Regulator guidance notes", "url": "https://inforegulator.org.za/guidance-notes/", - Network accessline 20
"name": "Information Regulator guidance notes", "url": "https://inforegulator.org.za/guidance-notes/", "enabled": true - Network accessline 21
"url": "https://inforegulator.org.za/guidance-notes/",
regwatch/state.json
- Network accessline 6
"media-statements": { "seen": [ "https://inforegulator.org.za/wp-content/uploads/2026/09/Final-MEDIA-INVITE-INFORMATION-REGULATOR-HOSTS-THE-INTERNATIONAL-DAY-FOR-UNIVERSAL-ACCESS-TO-INFORMATION-IDUAI-SESSION-IN-TSHWANE.pdf", - Network accessline 7
"seen": [ "https://inforegulator.org.za/wp-content/uploads/2026/09/Final-MEDIA-INVITE-INFORMATION-REGULATOR-HOSTS-THE-INTERNATIONAL-DAY-FOR-UNIVERSAL-ACCESS-TO-INFORMATION-IDUAI-SESSION-IN-TSHWANE.pdf", "https://inforegulato - Network accessline 8
"https://inforegulator.org.za/wp-content/uploads/2026/09/Final-MEDIA-INVITE-INFORMATION-REGULATOR-HOSTS-THE-INTERNATIONAL-DAY-FOR-UNIVERSAL-ACCESS-TO-INFORMATION-IDUAI-SESSION-IN-TSHWANE.pdf", - Network accessline 9
"https://inforegulator.org.za/wp-content/uploads/2026/08/MEDIA-INVITE-INFORMATION-REGULATOR-TO-HOST-MEDIA-BRIEFING-ON-HIGH-PROFILE-CASES-ENFORCEMENTS-AND-INVESTIGATIONS-INTO-POPIA-AND-PAIA-CONTRAVENTIONS.pdf", - Network accessline 10
"https://inforegulator.org.za/wp-content/uploads/2026/06/MEDIA-STATEMENT-INFORMATION-REGULATOR-ISSUES-ENFORCEMENT-NOTICES-FOR-THE-CONTRAVENTIONS-OF-POPIA-AND-PAIA-BY-PUBLIC-AND-PRIVATE-BODIES.pdf", - Network accessline 11
"https://inforegulator.org.za/wp-content/uploads/2026/06/MEDIA-INVITE-INFORMATION-REGULATOR-HOSTS-PROVINCIAL-ROADSHOW-IN-EASTERN-CAPE-TO-PROMOTE-POPIA-AND-PAIA.pdf", - Network accessline 12
"https://inforegulator.org.za/wp-content/uploads/2026/05/MEDIA-INVITE-INFORMATION-REGULATOR-HOSTS-PROVINCIAL-ROADSHOW-IN-EASTERN-CAPE-TO-PROMOTE-POPIA-AND-PAIA.pdf", - Network accessline 13
"https://inforegulator.org.za/wp-content/uploads/2026/05/Media-Alert-invitation-for-public-comment-on-POPIA-code-of-conduct-for-Gated-Access.pdf", - Network accessline 14
"https://inforegulator.org.za/wp-content/uploads/2026/04/INFORMATION-REGULATOR-TURNS-TO-THE-COURTS-TO-FINE-BLOUBERG-LOCAL-MUNICIPALITY-FOR-CONTRAVENTION-OF-POPIA_.pdf", - Network accessline 15
"https://inforegulator.org.za/wp-content/uploads/2026/04/MEDIA-STATEMENT-REGULATOR-NOTES-PUBLISHED-GAZETTE-OF-THE-CONSUMER-PROTECT-ACT-AMENDMENT-REGULATIONS-2026.pdf", - Network accessline 16
"https://inforegulator.org.za/wp-content/uploads/2026/02/APP-Stakeholder-Consultation-Session-Media-Invitation-final.pdf", - Network accessline 17
"https://inforegulator.org.za/wp-content/uploads/2026/02/MEDIA-ADVISORY_PRETORIA-CONFERENCE-06022026.pdf", - Network accessline 18
"https://inforegulator.org.za/wp-content/uploads/2026/02/MEDIA-INVITE-IDPD-2026.pdf", - Network accessline 19
"https://inforegulator.org.za/wp-content/uploads/2026/01/MEDIA-STATEMENT-THE-REGULATOR-APPEALS-THE-JUDGEMENT-ON-PUBLICATION-OF-MATRIC-RESULTS-31-DEC-2025.pdf", - Network accessline 20
"https://inforegulator.org.za/wp-content/uploads/2026/01/MEDIA-STATEMENT-THE-REGULATOR-TO-STUDY-THE-COURT-JUDGEMENT-ON-MATRIC-RESULTS-December-2025.pdf", - Network accessline 21
"https://inforegulator.org.za/wp-content/uploads/2025/11/MEDIA-STATEMENT-INFORMATION-REGULATOR-WELCOMES-THE-FOCUS-ON-DATA-PROTECTION-and-TRANSPARENCY-COMMITMENTS-IN-THE-G20.pdf", - Network accessline 22
"https://inforegulator.org.za/wp-content/uploads/2025/11/Media-Briefing-Invite-Updates-on-POPIA-and-PAIA-matters-nz.pdf", - Network accessline 23
"https://inforegulator.org.za/wp-content/uploads/2025/10/Media-Statement-COURT-RESERVES-JUDGEMENT-ON-THE-MATTER-OF-THE-PUBLICATION-OF-MATRIC-RESULTS.pdf", - Network accessline 24
"https://inforegulator.org.za/wp-content/uploads/2025/10/Media-Statement-IDUAI-2025-Final.pdf", - Network accessline 25
"https://inforegulator.org.za/wp-content/uploads/2025/09/Gov-Tech-Digital-Public-Service-Awards-Media-Statement-2025-final.pdf", - Network accessline 26
"https://inforegulator.org.za/wp-content/uploads/2025/07/Final-Media-Invite-Dikopano-NW-2025-pdf.pdf", - Network accessline 27
"https://inforegulator.org.za/wp-content/uploads/2025/05/MEDIA-STATEMENT-RELOCATION-OF-INFORMATION-REGULATORS-OFFICES-final.pdf", - Network accessline 28
"https://inforegulator.org.za/wp-content/uploads/2025/04/MEDIA-STATEMENT-INVITATION-TO-REPORT-SECURITY-COMPROMISES-THROUGH-THE-eSERVICES-PORTAL-.pdf", - Network accessline 29
"https://inforegulator.org.za/wp-content/uploads/2025/03/MEDIA-ALERT-SECTION-32-AND-83-4-PAIA-REPORTS.pdf", - Network accessline 30
"https://inforegulator.org.za/wp-content/uploads/2025/03/NOTICE-FOR-PAIA-ANNUAL-REPORT-2025.pdf", - Network accessline 31
"https://inforegulator.org.za/wp-content/uploads/2025/03/Media-Statement-IR-Welcomes-DOJ-CD-Minister-Decision-on-Sex-Offender-Register-Publishing-04-March-2025.pdf", - Network accessline 32
"https://inforegulator.org.za/wp-content/uploads/2025/03/Media-Invite-stakeholder-Engagement-Session-05-March-2025.pdf", - Network accessline 33
"https://inforegulator.org.za/wp-content/uploads/2025/01/MEDIA-INVITE-IDPD-2025.pdf", - Network accessline 34
"https://inforegulator.org.za/wp-content/uploads/2025/01/MEDIA-STATEMENT_-THE-SECURITY-COMPROMISE-AT-DBE-Final-1.pdf", - Network accessline 35
"https://inforegulator.org.za/wp-content/uploads/2025/01/FINAL-MEDIA-STATEMENT_INFORMATION-REGULATOR-and-DBE-MATRIC-RESULTS-COURT.pdf", - Network accessline 36
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-INVITE-IDPD-2024-2.pdf", - Network accessline 37
"https://inforegulator.org.za/wp-content/uploads/2020/07/FINAL-MEDIA-STATEMENT-IDPD-2024.pdf", - Network accessline 38
"https://inforegulator.org.za/wp-content/uploads/2020/07/FINAL-MEDIA-STATEMENT_INFORMATION-REGULATOR-WINS-BCX-INNOVATION-AWARD.pdf", - Network accessline 39
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT_INFORMATION-REGULATORS-LATEST-UPDATE-2.pdf", - Network accessline 40
"https://inforegulator.org.za/wp-content/uploads/2020/07/DIKOPANO-NORTHERN-CAPE-MEDIA-INVITE.pdf", - Network accessline 41
"https://inforegulator.org.za/wp-content/uploads/2020/07/IDUAI-Media-Statement-final.pdf", - Network accessline 42
"https://inforegulator.org.za/wp-content/uploads/2020/07/Media-Invite-for-Media-Briefing-PAIA-high-profile-cases-POPIA-progress.pdf", - Network accessline 43
"https://inforegulator.org.za/wp-content/uploads/2020/07/FINAL-DIKOPANO-FREE-STATE-MEDIA-INVITE.pdf", - Network accessline 44
"https://inforegulator.org.za/wp-content/uploads/2020/07/FINAL-IR-and-ESWATINI-COMMUNICATIONS-COMMISSION-MOU-MEDIA-STATEMENT.pdf", - Network accessline 45
"https://inforegulator.org.za/wp-content/uploads/2020/07/ANIC-2024-MEDIA-STATEMENT-final.pdf", - Network accessline 46
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-ON-GUIDANCE-NOTE-ON-POLITICAL-PARTIES-AND-INDEPENDENT-CANDIDATES-FINAL-1.pdf", - Network accessline 47
"https://inforegulator.org.za/wp-content/uploads/2020/07/Final-Media-Statement-PAIA-Annual-Report-Submission.pdf", - Network accessline 48
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-INVITE-Regulator_-IEC-_-MMA-multisectoral-dialogue.pdf", - Network accessline 49
"https://inforegulator.org.za/wp-content/uploads/2020/07/UPDATED_MEDIA-INVITE-INFORMATION-REGULATOR-SHARES-OUTCOMES-ON-INVESTIGATIONS-ASSESSMENTS.pdf", - Network accessline 50
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-INVITE-INFORMATION-REGULATOR-SHARES-OUTCOMES-ON-INVESTIGATIONS-ASSESSMENTS.pdf", - Network accessline 51
"https://inforegulator.org.za/wp-content/uploads/2020/07/Media-Statement-on-the-IEC-Security-Compromise.pdf", - Network accessline 52
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-ENFORCEMENT-NOTICE-ON-DIRECT-MARKETING-COMPLAINT.pdf", - Network accessline 53
"https://inforegulator.org.za/wp-content/uploads/2024/02/Public Notice Regarding Fraudulent activity.pdf", - Network accessline 54
"https://inforegulator.org.za/wp-content/uploads/2025/01/Media-Statement-Infringement-Notice-DBE.pdf", - Network accessline 55
"https://inforegulator.org.za/wp-content/uploads/2020/07/Statement-on-privacy-and-democratic-rights.pdf", - Network accessline 56
"https://inforegulator.org.za/wp-content/uploads/2020/07/FINAL-MEDIA-INVITE-FOR-THE-EASTERN-CAPE-DIKOPANO-and-STAKEHOLDER-ENGAGEMENT.pdf", - Network accessline 57
"https://inforegulator.org.za/wp-content/uploads/2020/07/FINAL-MEDIA-ADVISORY_-IDUAI-2023.pdf", - Network accessline 58
"https://inforegulator.org.za/wp-content/uploads/2020/07/FINAL-MEDIA-STATEMENT-ENFORCEMENT-NOTICE-ISSUED-TO-DISCHEM-PHARMACIES-LTD.pdf", - Network accessline 59
"https://inforegulator.org.za/wp-content/uploads/2020/07/FINAL-MEDIA-ADVISORY_-MPUMALANGA-COMMUNITY-ENGAGEMENT18.pdf", - Network accessline 60
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-INFRINGEMENT-NOTICE-ISSUED-TO-THE-DEPARTMENT-OF-JUSTICE-AND-CONSTITUTIONAL.pdf", - Network accessline 61
"https://inforegulator.org.za/wp-content/uploads/2020/07/Extension-notice-for-submission-of-Annual-Report-to-the-Regulator-in-terms-of-section-32-and-section.pdf", - Network accessline 62
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-THE-INFORMATION-REGULATOR-ELECTED-AS-EXECUTIVE-MEMBER-OF-A-GLOBAL-BODY.pdf", - Network accessline 63
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-THE-REGULATOR-WELCOMES-THE-JUDGMENT-OF-THE-CONSTITUTIONAL-COURT-IN-FAVOUR-OF-TRANSPARENCY_.pdf", - Network accessline 64
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-REGULATOR-CONDUCTS-PAIA-COMPLIANCE-ASSESSMENT-BY-POLITICAL-PARTIES.pdf", "https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-ENFORCEMENT-N - Network accessline 65
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-ENFORCEMENT-NOTICE-ISSUED-TO-THE-DEPARTMENT-OF-JUSTICE-AND-CONSTITUTIONAL-DEVELOPMENT-DUE-TO-CONTRAVENTION.pdf", "https://inforegulator.org.za/wp-content/uploa - Network accessline 66
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-INVITATION-TO-PUBLIC-and-PRIVATE-BODIES-TO-SUBMIT-ANNUAL-REPORTS-FOR-ACCESS-TO-INFO-REQUESTS-RECIEVED.pdf", - Network accessline 67
"https://inforegulator.org.za/wp-content/uploads/2020/07/pdf-Final-Version_MEDIA-BRIEFING-OF-INFORMATION-REGULATOR-ON-OUTCOMES-OF-RECEIVED-COMPLAINTS-003.pdf", - Network accessline 68
"https://inforegulator.org.za/wp-content/uploads/2020/07/Media-Statement_Information-Regulator-Refers-NDOH-to-the-Enforcement-Committee.pdf", - Network accessline 69
"https://inforegulator.org.za/wp-content/uploads/2020/07/Members-of-Parliament-demand-action-from-National-Treasury-on-independence-of-the-Information-Regulator_Approved.pdf", - Network accessline 70
"https://inforegulator.org.za/wp-content/uploads/2020/07/Final-MEDIA-ADVISORY-INTERNATIONAL-DATA-PRIVACY-DAY-2023.pdf", - Network accessline 71
"https://inforegulator.org.za/wp-content/uploads/2022/11/MEDIA%20STATEMENT-%20THE%20REGULATOR%20COMMENDS%20THE%20DEPARTMENT%20OF%20BASIC%20EDUCATION%20FOR%20COMPLIANCE%20WITH%20POPIA%20AND%20OBTAINING%20CONSENT_.pdf", - Network accessline 72
"https://inforegulator.org.za/wp-content/uploads/2022/11/Media%20Statement_Information%20Regulator's%20Enforcement%20Committee_Zooms%20in%20on%20Municipalities_v2.pdf", - Network accessline 73
"https://inforegulator.org.za/wp-content/uploads/2022/11/Media%20Invite%20_%20AFRICA%20DIDGITAL%20RIGHTS%20HUB.pdf", - Network accessline 74
"https://inforegulator.org.za/wp-content/uploads/2022/10/Media%20Statement%20Regulator%20Approves%20Codes%20of%20Conduct%20for%20the%20Banking%20and%20Credit%20Reporting%20Sectors.pdf", - Network accessline 75
"https://inforegulator.org.za/wp-content/uploads/2022/10/MEDIA%20INVITE_%20KZN%20ROADSHOW%20Final.pdf", - Network accessline 76
"https://inforegulator.org.za/wp-content/uploads/2020/07/Media-Invite-IDUAI-28-September-2022.pdf", - Network accessline 77
"https://inforegulator.org.za/wp-content/uploads/2020/07/Final-MEDIA-ALERT-_EXTENSION-FOR-SUBMISSION-OF-ANNUAL-REPORTS-IN-TERMS-OF-SECTION-32-of-PAIA-FOR-PUBLIC-BODIES.pdf", - Network accessline 78
"https://inforegulator.org.za/wp-content/uploads/2020/07/A-REMINDER-FOR-PUBLIC-BODIES-TO-SUBMIT-THEIR-ANNUAL-REPORT-IN-TERMS-OF-SECTION-32-OF-PROMOTION-OF-ACCESS-TO-INFORMATION-ACT-2-OF-2000-PAIA.pdf", - Network accessline 79
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT_-REGULATOR-ISSUES-A-SUMMONS-AGAINST-SOUTH-AFRICAN-POLICE-SERVICE-.pdf", - Network accessline 80
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-PUBLIC-HEARING-ON-ACCESS-TO-INFORMATION-ON-LICENSE-FEES-AND-COPYRIGHT-ROYALTIES-IN-THE-MUSIC-INDUSTRY.pdf", - Network accessline 81
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-THE-REGULATOR-INITIATES-AN-ASSESSMENT-INTO-POSSIBLE-VIOLATIONS-OF-POPIA-FOLLOWING-ALLEGATIONS-TO-PERSONAL-INFORMATION-LEAKED-BY-SAPS-0.pdf", - Network accessline 82
"https://inforegulator.org.za/wp-content/uploads/2020/07/Media-Statement_Information-Regulator-Establishes-Enforcement-Committee.pdf", - Network accessline 83
"https://inforegulator.org.za/wp-content/uploads/2020/07/Final-MEDIA-STATEMENT-INFORMATION-REGULATOR-MONITORS-THE-DEPARTMENT-OF-HEALTHS-POPIA-COMPLIANCE-22-APRIL-2022.pdf", - Network accessline 84
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-ANIC-14-APRIL-2022.pdf", - Network accessline 85
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-THE-REGULATOR-IS-DISSATISFIED-WITH-TRANSUNIONS-RESPONSE-AND-IT-INITIATES-AN-ASSESSMENT_FINAL.pdf", - Network accessline 86
"https://inforegulator.org.za/wp-content/uploads/2020/07/Human-Rights-Month-Webinar-Media-Statement-final.pdf", - Network accessline 87
"https://inforegulator.org.za/wp-content/uploads/2020/07/MEDIA-STATEMENT-The-Regulator-instructs-TransUnion-to-report-in-greater-detail-regarding-their-security-compromise.pdf", - Network accessline 88
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20220124-IDUAI-Webinar2021.pdf.pdf", - Network accessline 89
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20220912-PublishingMatricResults.pdf", - Network accessline 90
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20211202-appoints.pdf", - Network accessline 91
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20211027-Experian.pdf", - Network accessline 92
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20211014-ElectionCampaigning-POPIA.pdf", - Network accessline 93
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20210923-IDUAI-Webinar2021.pdf", - Network accessline 94
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20210913-ITsystems.pdf", - Network accessline 95
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20210713-ProcessingPersonalInfo.pdf", - Network accessline 96
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20210629-PAIA-SAHRC.pdf", - Network accessline 97
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20210622-Exemptions-LawfulProcessing.pdf", - Network accessline 98
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20210622-EnforcementPowers.pdf", - Network accessline 99
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20210401-GuidanceNote-IO.pdf", - Network accessline 100
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20210324-POPIA-Compliance.pdf", - Network accessline 101
"https://inforegulator.org.za/wp-content/uploads/2020/07/ms-20210303-Whatsapp.pdf" - Network accessline 105
"enforcement-notices": { "seen": [ "https://inforegulator.org.za/wp-content/uploads/2020/07/ENFORCEMENT-NOTICE-SAPS-MATTER-04052363.pdf", - Network accessline 106
"seen": [ "https://inforegulator.org.za/wp-content/uploads/2020/07/ENFORCEMENT-NOTICE-SAPS-MATTER-04052363.pdf", "https://inforegulator.org.za/wp-content/uploads/2020/07/ENFORCEMENT-NOTICE-HARDWICK-MATTER-04052375.pdf", - Network accessline 107
"https://inforegulator.org.za/wp-content/uploads/2020/07/ENFORCEMENT-NOTICE-SAPS-MATTER-04052363.pdf", - Network accessline 108
"https://inforegulator.org.za/wp-content/uploads/2020/07/ENFORCEMENT-NOTICE-HARDWICK-MATTER-04052375.pdf", - Network accessline 109
"https://inforegulator.org.za/wp-content/uploads/2020/07/ENFORCEMENT-NOTICE-DOJCD-MATTER-090523.pdf", - Network accessline 110
"https://inforegulator.org.za/wp-content/uploads/2020/07/ENFORCEMENT-NOTICE-IN-RESPECT-OF-THE-DEPARTMENT-OF-BASIC-EDUCATION-6-November-2024.pdf", - Network accessline 111
"https://inforegulator.org.za/wp-content/uploads/2020/07/FT-RAMS-CONSULTING-ENFORCEMENT-NOTICE-210224_Redacted.pdf", - Network accessline 112
"https://inforegulator.org.za/wp-content/uploads/2020/07/DIS-CHEM-ENFORCEMENT-NOTICE.pdf", - Network accessline 113
"https://inforegulator.org.za/wp-content/uploads/2025/04/WHATS-APP-ENFORCEMENT-NOTICE.pdf", - Network accessline 114
"https://inforegulator.org.za/wp-content/uploads/2026/06/GAUTENG-DEPARTMENT-OF-HEALTH-ENFORCEMENT-NOTICE-080626.pdf", - Network accessline 115
"https://inforegulator.org.za/wp-content/uploads/2026/06/SIBANYE-STILLWATER-ENFORCEMENT-NOTICE-220526-003_Redacted.pdf", - Network accessline 116
"https://inforegulator.org.za/wp-content/uploads/2026/05/CJC-ENFORCEMENT-NOTICE-220526-redact_Redacted.pdf" - Network accessline 120
"guidance-notes": { "seen": [ "https://inforegulator.org.za/wp-content/uploads/2020/07/GUIDANCE-NOTE-ON-DIRECT-MARKETING-IN-TERMS-OF-THE-PROTECTION-OF-PERSONAL-INFORMATION-ACT-4-OF-2013-POPIA.pdf", - Network accessline 121
"seen": [ "https://inforegulator.org.za/wp-content/uploads/2020/07/GUIDANCE-NOTE-ON-DIRECT-MARKETING-IN-TERMS-OF-THE-PROTECTION-OF-PERSONAL-INFORMATION-ACT-4-OF-2013-POPIA.pdf", "https://inforegulator.org.za/wp-content/uploa - Network accessline 122
"https://inforegulator.org.za/wp-content/uploads/2020/07/GUIDANCE-NOTE-ON-DIRECT-MARKETING-IN-TERMS-OF-THE-PROTECTION-OF-PERSONAL-INFORMATION-ACT-4-OF-2013-POPIA.pdf", - Network accessline 123
"https://inforegulator.org.za/wp-content/uploads/2020/07/FINAL-GUIDANCE-NOTE-ON-THE-PROCESSING-OF-PERSONAL-INFORMATION-OF-VOTERS-AND-THE-COUNTERING-OF-MISINFORMATION-AND-DISINFORMATION-DURING-ELECTIONS.pdf", - Network accessline 124
"https://inforegulator.org.za/wp-content/uploads/2020/07/Guidance-Note-Processing-Special-PersonalInformation-20210628-004.pdf", - Network accessline 125
"https://inforegulator.org.za/wp-content/uploads/2020/07/InfoRegSA-Form-Application-form-for-authorisation-to-process-Special-Personal-Information-eForm-2021-1.pdf", - Network accessline 126
"https://inforegulator.org.za/wp-content/uploads/2020/07/GuidanceNote-Processing-PersonalInformation-Children-20210628-1.pdf", - Network accessline 127
"https://inforegulator.org.za/wp-content/uploads/2020/07/InfoRegSA-Form-Application-form-for-authorisation-to-process-Personal-Information-of-Children-eForm-2021-1.pdf", - Network accessline 128
"https://inforegulator.org.za/wp-content/uploads/2020/07/InfoRegSA-GuidanceNote-PPI-Covid19-20200403.pdf", - Network accessline 129
"https://inforegulator.org.za/wp-content/uploads/2020/07/InfoRegSA-GuidanceNote-PPI-PolParties-1.pdf"
scripts/__init__.py
- Executable scriptline 1
Executable file
scripts/build_adapters.py
- Executable scriptline 1
#!/usr/bin/env python3 - Network accessline 63
"```sql\n" + INVENTORY_SQL + "\n```\n\n" "The same query and rules ship as `uvx popia-kit supabase sql` and `uvx popia-kit supabase check result.json`. " "For the live site, use the free passive check at https://popiakit.co. - Network accessline 65
"For the live site, use the free passive check at https://popiakit.co.za/check.\n")
scripts/cli.py
- Executable scriptline 1
#!/usr/bin/env python3 - Network accessline 6
popia-kit scan ./my-app code scanner (44 rules) popia-kit operators ./my-app third parties and the data sent to each popia-kit site-check https://example.com passive live site check - Network accessline 7
popia-kit operators ./my-app third parties and the data sent to each popia-kit site-check https://example.com passive live site check popia-kit supabase sql read-only Supabase database check - Network accessline 8
popia-kit site-check https://example.com passive live site check
scripts/popia_badge.py
- Executable scriptline 1
#!/usr/bin/env python3 - Network accessline 6
python3 scripts/popia_badge.py popia-scan.json --out popia-badge.svg python3 scripts/popia_badge.py popia-scan.json --shields-json --out popia-badge.json python3 scripts/site_check.py https://example.com --json site.json - Network accessline 7
python3 scripts/popia_badge.py popia-scan.json --shields-json --out popia-badge.json python3 scripts/site_check.py https://example.com --json site.json python3 scripts/popia_badge.py site.json --label "POPIA site check" --out site-b - Network accessline 8
python3 scripts/site_check.py https://example.com --json site.json - Network accessline 79
l, m = escape(label), escape(msg) title = escape(f"{label}: {msg}") return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{total}" height="20" role="img" aria-label="{title}"> - Network accessline 80
title = escape(f"{label}: {msg}") return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{total}" height="20" role="img" aria-label="{title}"> <title>{title}</title> - Network accessline 81
return f"""<svg xmlns="http://www.w3.org/2000/svg" width="{total}" height="20" role="img" aria-label="{title}">
scripts/popia_fleet.py
scripts/popia_init.py
- Executable scriptline 1
#!/usr/bin/env python3
scripts/popia_operators.py
scripts/popia_pack.py
- Executable scriptline 1
#!/usr/bin/env python3 - Network accessline 57
"[[VERIFY: Information Regulator contact details. Copy the current physical " "address, postal address, complaints email, general enquiries email and website " "from https://inforegulator.org.za. Do not copy details from old documen - Network accessline 58
"address, postal address, complaints email, general enquiries email and website " "from https://inforegulator.org.za. Do not copy details from old documents or " "other organisations' notices.]]" - Network accessline 59
"from https://inforegulator.org.za. Do not copy details from old documents or " - Network accessline 63
REPO_URL = "https://github.com/VenusOne-Lee/popia-compliance" - Network accessline 760
lines.append("") lines.append("- **Information Regulator contact details** are a placeholder in every file that needs them. " "Copy the current details from https://inforegulator.org.za. This tool never supplies them.") - Network accessline 761
lines.append("- **Information Regulator contact details** are a placeholder in every file that needs them. " "Copy the current details from https://inforegulator.org.za. This tool never supplies them.") vitems = _verify - Network accessline 762
"Copy the current details from https://inforegulator.org.za. This tool never supplies them.") - Network accessline 862
"trading_name": "Example Lodge", "physical_address": "1 Example Road, Example Town, 0000, South Africa (fictional)", "website": "https://examplelodge.example", - Network accessline 863
"physical_address": "1 Example Road, Example Town, 0000, South Africa (fictional)", "website": "https://examplelodge.example", "head_of_body": "Thandi Example (Managing Director)", - Network accessline 864
"website": "https://examplelodge.example",
scripts/popia_scan.py
- Executable scriptline 1
#!/usr/bin/env python3 - Executable scriptline 1
#!/usr/bin/env python3 - Access to secretsline 200
[Credential-related source omitted] - Access to secretsline 200
[Credential-related source omitted] - Access to secretsline 205
[Credential-related source omitted] - Access to secretsline 205
[Credential-related source omitted] - Network accessline 516
}) return json.dumps({ "$schema": "https://json.schemastore.org/sarif-2.1.0.json", "version": "2.1.0", - Network accessline 516
}) return json.dumps({ "$schema": "https://json.schemastore.org/sarif-2.1.0.json", "version": "2.1.0", - Network accessline 517
return json.dumps({ "$schema": "https://json.schemastore.org/sarif-2.1.0.json", "version": "2.1.0", "runs": [{"tool": {"driver": {"name": "popia_scan", "version": res["version"], - Network accessline 517
return json.dumps({ "$schema": "https://json.schemastore.org/sarif-2.1.0.json", "version": "2.1.0", "runs": [{"tool": {"driver": {"name": "popia_scan", "version": res["version"], - Network accessline 518
"$schema": "https://json.schemastore.org/sarif-2.1.0.json", "version": "2.1.0", - Network accessline 518
"$schema": "https://json.schemastore.org/sarif-2.1.0.json", "version": "2.1.0", - Network accessline 519
"runs": [{"tool": {"driver": {"name": "popia_scan", "version": res["version"], "informationUri": "https://github.com/VenusOne-Lee/popia-compliance", "rules": rule_m - Network accessline 519
"runs": [{"tool": {"driver": {"name": "popia_scan", "version": res["version"], "informationUri": "https://github.com/VenusOne-Lee/popia-compliance", "rules": rule_m - Network accessline 520
"informationUri": "https://github.com/VenusOne-Lee/popia-compliance", - Network accessline 520
"informationUri": "https://github.com/VenusOne-Lee/popia-compliance",
scripts/popia_vendors.py
- Executable scriptline 1
Executable file
scripts/regwatch.py
- Executable scriptline 1
#!/usr/bin/env python3 - Network accessline 39
DEFAULT_CONFIG = { "user_agent": "popia-compliance-regwatch/1.0 (+https://github.com/VenusOne-Lee/popia-compliance)", - Network accessline 40
"user_agent": "popia-compliance-regwatch/1.0 (+https://github.com/VenusOne-Lee/popia-compliance)", - Network accessline 156
def fetch(url: str, ua: str, timeout: float) -> str: - Network accessline 157
if not url.startswith("https://"): raise ValueError("regwatch sources must be https URLs") req = urllib.request.Request(url, headers={"User-Agent": ua, "Accept": "text/html"}) - Network accessline 159
req = urllib.request.Request(url, headers={"User-Agent": ua, "Accept": "text/html"}) - Network accessline 161
with urllib.request.urlopen(req, timeout=timeout) as r: # nosec B310 - Network accessline 253
if n: time.sleep(delay) html = fetch(src["url"], ua, timeout) - Network accessline 254
time.sleep(delay) html = fetch(src["url"], ua, timeout) res["items"] = extract_items(html, src["url"], config) - Network accessline 255
html = fetch(src["url"], ua, timeout)
scripts/rules/__init__.py
- Executable scriptline 1
Executable file
scripts/rules/access.py
- Executable scriptline 1
Executable file
scripts/rules/consent.py
- Executable scriptline 1
Executable file
scripts/rules/pii.py
scripts/rules/repo.py
- Executable scriptline 1
Executable file
scripts/rules/secrets.py
scripts/rules/thirdparty.py
- Executable scriptline 1
Executable file
scripts/rules/web.py
- Executable scriptline 1
Executable file - Network accessline 170
Rule( id="SEC-009", title="Unencrypted http:// endpoint", severity="medium", popia="s19", pattern=r"""['"`]http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|www\.w3\.org|schemas\.| - Network accessline 171
id="SEC-009", title="Unencrypted http:// endpoint", severity="medium", popia="s19", pattern=r"""['"`]http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|www\.w3\.org|schemas\.|xmlns|example\ - Network accessline 172
pattern=r"""['"`]http://(?!localhost|127\.0\.0\.1|0\.0\.0\.0|\[::1\]|10\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|www\.w3\.org|schemas\.|xmlns|example\.(com|org)|purl\.org|ns\.adobe)[^'"`\s]+""", - Access to secretsline 224
[Credential-related source omitted]
scripts/sa_id.py
- Executable scriptline 1
#!/usr/bin/env python3
scripts/site_check.py
- Executable scriptline 1
#!/usr/bin/env python3 - Network accessline 9
python3 site_check.py https://example.co.za [--out report.md] [--json report.json] [--timeout 15] - Network accessline 11
Programmatic: from site_check import check, fmt_md res = check("https://example.co.za") - Network accessline 13
res = check("https://example.co.za") - Network accessline 38
__version__ = "1.0.1" BOT_INFO_URL = "https://github.com/VenusOne-Lee/popia-compliance/blob/main/docs/site-check-bot.md" - Network accessline 39
BOT_INFO_URL = "https://github.com/VenusOne-Lee/popia-compliance/blob/main/docs/site-check-bot.md" - Network accessline 124
hint = ("Scan your code instead, bot protection does not affect it: popia-kit scan . " "Or add the POPIA Kit skill to your AI tool: " "https://github.com/VenusOne-Lee/popia-compliance#whats-inside . " - Network accessline 125
"Or add the POPIA Kit skill to your AI tool: " "https://github.com/VenusOne-Lee/popia-compliance#whats-inside . " "If it is your site, you can allow the checker for one check: " + BOT_INFO_URL) - Network accessline 126
"https://github.com/VenusOne-Lee/popia-compliance#whats-inside . " - Network accessline 168
ctx = ssl.create_default_context() _check_target(url, block_private) req = urllib.request.Request(url, method=method, headers={"User-Agent": UA, "Accept": "text/html,*/*"}) - Network accessline 170
req = urllib.request.Request(url, method=method, headers={"User-Agent": UA, "Accept": "text/html,*/*"})
scripts/style_lint.py
- Executable scriptline 1
#!/usr/bin/env python3
scripts/supabase_check.py
server.json
- Network accessline 1
{ "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", - Network accessline 2
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", - Network accessline 4
"title": "POPIA Kit", "description": "POPIA privacy and security checks for South African apps: code scan, site check, guidance.", "websiteUrl": "https://popiakit.co.za", - Network accessline 5
"description": "POPIA privacy and security checks for South African apps: code scan, site check, guidance.", "websiteUrl": "https://popiakit.co.za", "repository": { - Network accessline 6
"websiteUrl": "https://popiakit.co.za", - Network accessline 7
"repository": { "url": "https://github.com/VenusOne-Lee/popia-compliance", "source": "github" - Network accessline 8
"url": "https://github.com/VenusOne-Lee/popia-compliance", - Network accessline 13
{ "registryType": "pypi", "registryBaseUrl": "https://pypi.org", - Network accessline 14
"registryType": "pypi", "registryBaseUrl": "https://pypi.org", "identifier": "popia-kit", - Network accessline 15
"registryBaseUrl": "https://pypi.org",
templates/breach-notice-data-subject.md
- Network accessline 51
You also have the right to lodge a complaint with the Information Regulator. {{regulator_contact}} Verify the current contact details on https://inforegulator.org.za.
templates/breach-regulator-checklist.md
- Network accessline 7
This checklist lists the information to **have ready** before logging on. It does not reproduce the portal's field names, which may change. Check the portal and the Regulator's current guidance on https://inforegulator.org.za.
templates/dsar-runbook.md
- Network accessline 133
Every refusal or partial response must tell the data subject they may complain to the Information Regulator. {{regulator_contact}} Verify current contact details on https://inforegulator.org.za.
templates/pack-questionnaire.json
- Network accessline 23
{"id": "physical_address", "section": "Company", "prompt": "Physical address", "type": "text", "required": true, "default": null, "help": "Street address of the responsible party. s18(1)(b)."}, {"id": "postal_address", "section": "Compa - Network accessline 24
{"id": "postal_address", "section": "Company", "prompt": "Postal address (if different)", "type": "text", "required": false, "default": null, "help": "Leave blank if the same as the physical address."}, {"id": "website", "section": "Com - Network accessline 25
{"id": "website", "section": "Company", "prompt": "Website", "type": "url", "required": true, "default": null, "help": "Main website, for example https://example.co.za."},
templates/paia-manual-scaffold.md
- Network accessline 7
**Important:** the Information Regulator publishes a guide and template for PAIA manuals and may update them. Check the current template and guide on https://inforegulator.org.za before publishing, and align this scaffold to it. Requirement - Network accessline 44
Verify current contact details on https://inforegulator.org.za.
templates/privacy-notice.md
- Network accessline 231
Contact details change. Verify the current details on the Regulator's website, https://inforegulator.org.za, before relying on them.
tests/__init__.py
- Executable scriptline 1
Executable file
tests/automation/__init__.py
- Executable scriptline 1
Executable file
tests/automation/fixtures/__init__.py
- Executable scriptline 1
Executable file
tests/automation/fixtures/regulator-list.html
- Network accessline 9
<a href="/about/">About</a> <a href="#content">Skip to content</a> <a href="https://www.facebook.com/example">Facebook profile for the example site</a> - Network accessline 10
<a href="#content">Skip to content</a> <a href="https://www.facebook.com/example">Facebook profile for the example site</a> </nav> - Network accessline 11
<a href="https://www.facebook.com/example">Facebook profile for the example site</a> - Network accessline 16
<ul class="items"> <li><a href="/wp-content/uploads/2026/09/Fixture-Statement-Alpha.pdf">Fixture statement alpha on test topic one</a> <span>1 September 2026</span></li> <li><a href="https://example.test/wp-content/uploads/2026/08/F - Network accessline 17
<li><a href="/wp-content/uploads/2026/09/Fixture-Statement-Alpha.pdf">Fixture statement alpha on test topic one</a> <span>1 September 2026</span></li> <li><a href="https://example.test/wp-content/uploads/2026/08/Fixture-Notice-Beta.pdf" - Network accessline 18
<li><a href="https://example.test/wp-content/uploads/2026/08/Fixture-Notice-Beta.pdf">
tests/automation/test_badge.py
- Executable scriptline 1
Executable file
tests/automation/test_cli.py
- Executable scriptline 1
Executable file
tests/automation/test_fleet.py
- Executable scriptline 1
Executable file
tests/automation/test_init.py
- Executable scriptline 1
Executable file
tests/automation/test_mcp.py
tests/automation/test_regwatch.py
- Executable scriptline 1
Executable file - Network accessline 13
FIXTURE = ROOT / "tests" / "automation" / "fixtures" / "regulator-list.html" BASE = "https://inforegulator.org.za/media-statements/" - Network accessline 14
BASE = "https://inforegulator.org.za/media-statements/" - Network accessline 33
def test_resolves_relative_links(self): self.assertEqual(self.items[0]["url"], "https://inforegulator.org.za/wp-content/uploads/2026/09/Fixture-Statement-Alpha.pdf") - Network accessline 34
self.assertEqual(self.items[0]["url"], "https://inforegulator.org.za/wp-content/uploads/2026/09/Fixture-Statement-Alpha.pdf") self.assertTrue(self.items[1]["url"].startswith("https://example.test/")) - Network accessline 35
"https://inforegulator.org.za/wp-content/uploads/2026/09/Fixture-Statement-Alpha.pdf") - Network accessline 36
self.assertTrue(self.items[1]["url"].startswith("https://example.test/")) - Network accessline 78
def test_sources_config_has_required_pages(self): urls = {s["url"] for s in config()["sources"]} self.assertIn("https://inforegulator.org.za/media-statements/", urls) - Network accessline 79
urls = {s["url"] for s in config()["sources"]} self.assertIn("https://inforegulator.org.za/media-statements/", urls) self.assertIn("https://inforegulator.org.za/enforcement-notices/", urls) - Network accessline 80
self.assertIn("https://inforegulator.org.za/media-statements/", urls) - Network accessline 81
self.assertIn("https://inforegulator.org.za/enforcement-notices/", urls)
tests/fixtures/demo-app/index.html
- Network accessline 3
<head> <!-- Deliberately non-compliant demo fixture for popia_scan tests. Synthetic data only. --> <script async src="https://www.googletagmanager.com/gtag/js?id=G-DEMO000000"></script> - Network accessline 4
<!-- Deliberately non-compliant demo fixture for popia_scan tests. Synthetic data only. --> <script async src="https://www.googletagmanager.com/gtag/js?id=G-DEMO000000"></script> <script>fbq('init', '000000000000000');</script> - Network accessline 5
<script async src="https://www.googletagmanager.com/gtag/js?id=G-DEMO000000"></script>
tests/fixtures/demo-app/package.json
- Executable scriptline 1
Executable file
tests/fixtures/demo-app/src/ai.ts
- Executable scriptline 1
Executable file
tests/fixtures/demo-app/src/components/SignupForm.tsx
- Executable scriptline 1
Executable file - Network accessline 8
console.log("new signup", email, req.body); localStorage.setItem("user_email", email); await fetch(`http://api.demo-app.co.za/lead?email=${email}`); - Network accessline 9
localStorage.setItem("user_email", email); await fetch(`http://api.demo-app.co.za/lead?email=${email}`); } - Network accessline 10
await fetch(`http://api.demo-app.co.za/lead?email=${email}`);
tests/fixtures/demo-app/src/newsletter.ts
- Executable scriptline 1
Executable file
tests/fixtures/demo-app/supabase/functions/lead/index.ts
- Executable scriptline 1
Executable file
tests/fixtures/operators-app/.env.example
- Network accessline 1
SUPABASE_URL=https://fixture-project.supabase.co
tests/fixtures/operators-app/api/triage.py
- Executable scriptline 1
Executable file
tests/fixtures/operators-app/package.json
- Executable scriptline 1
Executable file
tests/fixtures/operators-app/public/contact.html
- Network accessline 1
<!-- Synthetic fixture for the operator inventory. --> <form action="https://formspree.io/f/fixture" method="post"> - Network accessline 2
<form action="https://formspree.io/f/fixture" method="post"> - Network accessline 4
<input name="phone" type="tel"> </form> <a href="https://www.w3.org/TR/html52/">spec</a> - Network accessline 6
<a href="https://www.w3.org/TR/html52/">spec</a>
tests/fixtures/operators-app/src/checkout.ts
tests/fixtures/operators-app/src/sms.js
- Executable scriptline 1
Executable file - Access to secretsline 3
[Credential-related source omitted] - Network accessline 9
async function crm(lead) { await fetch("https://hooks.zapier.com/hooks/catch/0/fixture", { method: "POST", body: JSON.stringify({ email: lead.email }) }); - Network accessline 10
await fetch("https://hooks.zapier.com/hooks/catch/0/fixture", { method: "POST", body: JSON.stringify({ email: lead.email }) }); - Network accessline 11
await fetch("https://api.partner-crm.example.co/leads", { method: "POST", body: JSON.stringify({ phone: lead.phone }) }); - Network accessline 12
await fetch("https://api.operators-app.co.za/internal", { method: "POST", body: JSON.stringify({ phone: lead.phone }) });
tests/fixtures/operators-app/supabase-edge.ts
tests/fixtures/operators-app/tests/checkout.test.ts
- Executable scriptline 1
Executable file
tests/pack/__init__.py
- Executable scriptline 1
Executable file
tests/pack/kasi-bookings-answers.json
- Network accessline 5
"trading_name": "Kasi Bookings", "physical_address": "10 Test Street, Soweto, Johannesburg, 0000 (fictional)", "website": "https://kasibookings.example", - Network accessline 6
"physical_address": "10 Test Street, Soweto, Johannesburg, 0000 (fictional)", "website": "https://kasibookings.example", "head_of_body": "Naledi Test (CEO)", - Network accessline 7
"website": "https://kasibookings.example",
tests/pack/test_pack.py
- Executable scriptline 1
Executable file
tests/scanner/__init__.py
- Executable scriptline 1
Executable file
tests/scanner/test_baseline.py
- Executable scriptline 1
Executable file
tests/scanner/test_operators.py
- Executable scriptline 1
Executable file - Network accessline 135
self.assertEqual(po._js_package("@supabase/supabase-js/dist/x"), "@supabase/supabase-js") self.assertEqual(po._js_package("npm:openai@4.2.0"), "openai") self.assertEqual(po._js_package("https://esm.sh/@supabase/supabase-js@2 - Network accessline 136
self.assertEqual(po._js_package("npm:openai@4.2.0"), "openai") self.assertEqual(po._js_package("https://esm.sh/@supabase/supabase-js@2"), "@supabase/supabase-js") self.assertIsNone(po._js_package("./local")) - Network accessline 137
self.assertEqual(po._js_package("https://esm.sh/@supabase/supabase-js@2"), "@supabase/supabase-js") - Network accessline 159
with tempfile.TemporaryDirectory() as d: Path(d, "a.ts").write_text( f'fetch("https://hooks.zapier.com/x?id={fid}&e={email}", {{ method: "POST" }});\n', - Network accessline 160
Path(d, "a.ts").write_text( f'fetch("https://hooks.zapier.com/x?id={fid}&e={email}", {{ method: "POST" }});\n', encoding="utf-8") - Network accessline 161
f'fetch("https://hooks.zapier.com/x?id={fid}&e={email}", {{ method: "POST" }});\n',
tests/scanner/test_rules.py
tests/scanner/test_site_check.py
- Executable scriptline 1
Executable file - Network accessline 13
PAGE = b"""<!doctype html><html><head> <script async src="https://www.googletagmanager.com/gtag/js?id=G-TEST"></script> - Network accessline 14
<script async src="https://www.googletagmanager.com/gtag/js?id=G-TEST"></script> - Network accessline 42
cls.t = threading.Thread(target=cls.srv.serve_forever, daemon=True) cls.t.start() cls.res = site_check.check(f"http://127.0.0.1:{cls.srv.server_port}/", timeout=5) - Network accessline 44
cls.res = site_check.check(f"http://127.0.0.1:{cls.srv.server_port}/", timeout=5) - Network accessline 94
cls.srv = HTTPServer(("127.0.0.1", 0), Unreadable) threading.Thread(target=cls.srv.serve_forever, daemon=True).start() cls.base = f"http://127.0.0.1:{cls.srv.server_port}" - Network accessline 96
cls.base = f"http://127.0.0.1:{cls.srv.server_port}"
tests/security/__init__.py
- Executable scriptline 1
Executable file
tests/security/test_hardening.py
- Executable scriptline 1
Executable file - Access to secretsline 32
[Credential-related source omitted] - Network accessline 33
[Credential-related source omitted] - Network accessline 34
[Credential-related source omitted] - Network accessline 35
[Credential-related source omitted] - Network accessline 45
[Credential-related source omitted] - Network accessline 46
[Credential-related source omitted] - Network accessline 47
[Credential-related source omitted] - Access to secretsline 66
[Credential-related source omitted] - Access to secretsline 67
[Credential-related source omitted] - Access to secretsline 68
[Credential-related source omitted] - Network accessline 73
def test_block_private_refuses_internal_targets(self): with self.assertRaises(site_check.BlockedTarget): site_check.check(f"http://127.0.0.1:{self.srv.server_port}/", timeout=5, block_private=True) - Network accessline 74
with self.assertRaises(site_check.BlockedTarget): site_check.check(f"http://127.0.0.1:{self.srv.server_port}/", timeout=5, block_private=True) for bad in ("http://169.254.169.254/latest/meta-data/", "http://10.0.0.1/", " - Network accessline 75
site_check.check(f"http://127.0.0.1:{self.srv.server_port}/", timeout=5, block_private=True) - Network accessline 76
for bad in ("http://169.254.169.254/latest/meta-data/", "http://10.0.0.1/", "http://[::1]/"): - Network accessline 85
[Credential-related source omitted] - Network accessline 86
[Credential-related source omitted] - Network accessline 87
[Credential-related source omitted] - Network accessline 99
[Credential-related source omitted] - Network accessline 100
[Credential-related source omitted] - Access to secretsline 116
[Credential-related source omitted] - Network accessline 137
def test_site_check_tool_blocks_private_addresses(self): out = self.s.call_tool({"name": "popia_site_check", "arguments": {"url": "http://127.0.0.1:9/"}}) - Network accessline 138
out = self.s.call_tool({"name": "popia_site_check", "arguments": {"url": "http://127.0.0.1:9/"}}) - Network accessline 144
def test_clone_rejects_unsafe_urls(self): for bad in ("--upload-pack=touch /tmp/pwned", "file:///etc", "/local/repo", "ext::sh -c id", "http://insecure.test/repo.git", "-c core.sshCommand=id"): - Network accessline 145
for bad in ("--upload-pack=touch /tmp/pwned", "file:///etc", "/local/repo", "ext::sh -c id", "http://insecure.test/repo.git", "-c core.sshCommand=id"): with self.assertRaises(ValueError, msg=bad): - Network accessline 146
"http://insecure.test/repo.git", "-c core.sshCommand=id"): - Network accessline 150
def test_git_error_redacts_tokens(self): msg = popia_fleet._redact_git_error(f"fatal: could not read https://{SECRET}@github.com/x/y.git") - Network accessline 151
msg = popia_fleet._redact_git_error(f"fatal: could not read https://{SECRET}@github.com/x/y.git")
tests/supabase/__init__.py
- Executable scriptline 1
Executable file
tests/supabase/run_live.sh
tests/supabase/test_supabase_check.py
- Executable scriptline 1
Executable file - Access to secretsline 156
[Credential-related source omitted] - Access to secretsline 157
[Credential-related source omitted] - Network accessline 161
self.assertIn("SUPABASE_DB_URL", err) marker = "not" + "-a-real-" + "value" os.environ["SUPABASE_DB_URL"] = "https://user:" + marker + "@example.com/db" - Network accessline 162
marker = "not" + "-a-real-" + "value" os.environ["SUPABASE_DB_URL"] = "https://user:" + marker + "@example.com/db" code, _, err = run("run") - Network accessline 163
[Credential-related source omitted] - Access to secretsline 163
[Credential-related source omitted] - Access to secretsline 168
[Credential-related source omitted] - Access to secretsline 169
[Credential-related source omitted] - Access to secretsline 180
[Credential-related source omitted] - Access to secretsline 188
[Credential-related source omitted] - Access to secretsline 191
[Credential-related source omitted] - Access to secretsline 192
[Credential-related source omitted] - Access to secretsline 197
[Credential-related source omitted] - Access to secretsline 199
[Credential-related source omitted] - Access to secretsline 212
[Credential-related source omitted] - Access to secretsline 214
[Credential-related source omitted] - Access to secretsline 221
[Credential-related source omitted] - Access to secretsline 223
[Credential-related source omitted]
How the signals fit together
Facts from a static read of the files at the pinned commit. They describe what the code does, not why.
Some files could not be read; the facts below still hold.
Declared and executed: 5 facts
- Contacts www.python.org, which SKILL.md, the README and the plugin manifest do not mentionbin/popia-kit.js:25High confidence
- Contacts api.anthropic.com, which SKILL.md, the README and the plugin manifest do not mentionevals/run_evals.py:288High confidence
- Contacts api.openai.com, which SKILL.md, the README and the plugin manifest do not mentionevals/run_evals.py:294High confidence
- Contacts cloudflare-dns.com, which SKILL.md, the README and the plugin manifest do not mentionhosted/site-check/supabase/functions/site-check/core.ts:48High confidence
- Contacts challenges.cloudflare.com, which SKILL.md, the README and the plugin manifest do not mentionhosted/site-check/supabase/functions/site-check/core.ts:49High confidence
Signal confidence: 495 facts
- Network access in prose of documentationCHANGELOG.md:44Low confidence
- Network access in prose of documentationCHANGELOG.md:45Low confidence
- Network access in prose of documentationCHANGELOG.md:46Low confidence
- Network access in prose of documentationCHANGELOG.md:73Low confidence
- Network access in prose of documentationCHANGELOG.md:74Low confidence
- Network access in prose of documentationCHANGELOG.md:75Low confidence
- Network access in prose of documentationCODE_OF_CONDUCT.md:3Low confidence
- Network access in prose of documentationGOVERNANCE.md:23Low confidence
- Network access in prose of documentationREADME.md:3Low confidence
- Network access in prose of documentationREADME.md:41Low confidence
- Network access in prose of documentationREADME.md:61Low confidence
- Network access in prose of documentationREADME.md:74Low confidence
- Network access in prose of documentationREADME.md:81Low confidence
- Network access in prose of documentationREADME.md:82Low confidence
- Network access in prose of documentationREADME.md:83Low confidence
- Network access in code of documentationREADME.md:93Low confidence
- Network access in prose of documentationREADME.md:135Low confidence
- Network access in prose of documentationREADME.md:136Low confidence
- Network access in prose of documentationREADME.md:137Low confidence
- Network access in code of the SKILL.mdSKILL.md:81High confidence
- Network access in prose of documentationadapters/_core.md:5Low confidence
- Network access in prose of documentationadapters/agents/AGENTS.md:6Low confidence
- Network access in prose of documentationadapters/chatgpt/custom-gpt-instructions.md:6Low confidence
- Network access in prose of documentationadapters/claude/CLAUDE.md:6Low confidence
- Network access in prose of documentationadapters/copilot/.github/copilot-instructions.md:6Low confidence
- Network access in prose of documentationadapters/gemini/GEMINI.md:6Low confidence
- Network access in prose of documentationadapters/lovable/knowledge.md:6Low confidence
- Network access in prose of the SKILL.mdskill/SKILL.md:12Low confidence
- Network access in prose of the SKILL.mdskill/SKILL.md:170Low confidence
- Network access in prose of documentationadapters/windsurf/.windsurf/rules/popia.md:10Low confidence
- Executable script in code of a file that runsbin/popia-kit.js:1High confidence
- Network access in code of a file that runsbin/popia-kit.js:23High confidence
- Network access in code of a file that runsbin/popia-kit.js:24High confidence
- Network access in code of a file that runsbin/popia-kit.js:25High confidence
- Network access in code of documentationdocs/automation.md:99Low confidence
- Network access in code of documentationdocs/automation.md:100Low confidence
- Network access in code of documentationdocs/automation.md:204Low confidence
- Network access in code of documentationdocs/automation.md:205Low confidence
- Network access in prose of documentationdocs/automation.md:228Low confidence
- Network access in code of documentationdocs/automation.md:231Low confidence
- Access to secrets in prose of documentation: a variable, placeholder or call, not a stored valuedocs/mcp.md:149Low confidence
- Network access in prose of documentationdocs/quickstart.md:15Low confidence
- Network access in code of documentationdocs/quickstart.md:26Low confidence
- Network access in code of documentationdocs/quickstart.md:95Low confidence
- Network access in prose of documentationdocs/releasing.md:53Low confidence
- Network access in prose of documentationdocs/releasing.md:58Low confidence
- Network access in prose of documentationdocs/releasing.md:60Low confidence
- Network access in prose of documentationdocs/site-check-bot.md:7Low confidence
- Network access in code of documentationdocs/site-check-bot.md:12Low confidence
- Network access in prose of documentationdocs/site-check-bot.md:19Low confidence
and 445 more
Only the first 500 facts are listed.
Works with
| Agent | Status | Evidence | Install to |
|---|---|---|---|
| Claude Code | Compatible | Standard SKILL.md SKILL.md | .claude/skills/<skill-name>/ |
| Codex | Native | Manifest adapters/agents/AGENTS.md | .agents/skills/<skill-name>/ |
| Cursor | Native | Path adapters/cursor/.cursor/rules/popia.mdc | .cursor/skills/<skill-name>/ |
| Gemini CLI | Compatible | Standard SKILL.md SKILL.md | .gemini/skills/<skill-name>/ |
| GitHub Copilot | Compatible | Standard SKILL.md SKILL.md | .github/skills/<skill-name>/ |
| OpenCode | Compatible | Standard SKILL.md SKILL.md | .opencode/skills/<skill-name>/ |
- Native The repository ships a manifest, folder or frontmatter field for this agent.
- Compatible A standard SKILL.md that this agent can read.
- Unknown No evidence either way.
Derived automatically from paths, manifests and frontmatter. Not tested on each agent.