A web-based tool that utilizes Azure Foundry Tools and Azure AI Content Understanding to indentify hidden dark patterns in websites.
Dark patterns are design strategies intentionally crafted to manipulate users into actions they might not otherwise take. These actions typically favor business goals over user autonomy, clarity, or consent. They rely on deceptive UI choices, emotional pressure, or forced decisions making it difficult for users to make informed or voluntary choices.
Ethically, dark patterns are dishonest. They are often driven by marketing and sales pressures, reusing psychological tactics that may improve short-term metrics but damage long-term trust. From a user’s perspective, these patterns feel invasive, manipulative, and exhausting — pushing people to abandon flows, products, or even entire brands.
You give it a website address. It opens the site in a real browser, clicks through the cookie banner the way a visitor would, and tells you which manipulative design tricks it found — and which GDPR article each one breaks.
Current features
- Explores the consent flow on its own. Clicks Accept, Reject, Manage, opens preference panels, and follows the flow through nested screens — including panels that only appear after a click.
- Detects 14 dark patterns, each mapped to the GDPR article it puts at risk — pre-ticked tracking, a hidden reject button, a maze of screens to refuse, buttons that do nothing, no way to withdraw consent, and more.
- Measures the page, doesn't guess. Colour contrast, font size and button area are read from the live web page, so a claim like "the reject link is 1.9:1 contrast" is a measurement, not an AI opinion.
- Points at the evidence. Every finding highlights the exact button on a screenshot of the real page.
- Shows the full consent flow as a map, so you can see how many clicks accepting takes versus refusing.
- Reports nothing when there is nothing to report. A clean site returns an empty result rather than an invented one.
- Protects itself from the sites it audits. A page cannot hide instructions in its text or images to talk the AI out of reporting it.
- Shareable reports. Each completed scan gets its own link.
Findings are advisory, not legal advice.
target URL → crawl → prune → Content Understanding → Prompt Shields → agent → report
- Target URL — you submit a website address in the web app.
- Crawl — a real browser opens the site with no saved cookies and clicks through every consent choice it can find, saving a screenshot of each screen it reaches.
- Prune — most of what a page contains is irrelevant. Only the screens where a real decision happens are kept, which keeps the AI cost down.
- Content Understanding — reads each saved screenshot and returns the text on it plus the position of every button and toggle.
- Prompt Shields — checks the website's own words for hidden instructions aimed at the AI, before the AI ever sees them.
- Agent — weighs the measurements against fixed rules and decides which dark patterns are actually proven.
- Report — the findings, each with its GDPR article and the exact button highlighted on the screenshot.
azure/ Configuration and prompts used for azure tools.
backend/ Python + FastAPI
app/ API, crawler, Azure pipeline
playbooks/ Cookie banner selectors
tests/ Offline test suite
frontend/ React + Typescript
test-run/ sample test run reports
You need Python 3.11+, Node 20+, and the Azure CLI. Sign in first with
az login — the app uses that login to reach Azure.
Backend (first terminal):
cd backend
python3 -m venv .venv # create a virtual environment
source .venv/bin/activate # switch into it
pip install -r requirements.txt # install Python packages
playwright install chromium # download the browser it drives
cp .env.example .env # then fill in your Azure values (see below)
uvicorn app.main:app --port 8000Frontend (second terminal):
cd frontend
npm install
npm run devOpen http://localhost:5173.
Or run everything in Docker instead:
docker compose up --build # then open http://localhost:8080Containers cannot use your az login. To run this way, add
AZURE_CLIENT_ID, AZURE_TENANT_ID and AZURE_CLIENT_SECRET for a service
principal to backend/.env. For everyday work the two-terminal setup above is
simpler.
Create the service principal with:
az ad sp create-for-rbac --name dark-pattern-detector-localThen give it the same roles listed under Azure setup below. The roles must be assigned on every resource group involved — the AI resources and the storage account may live in different ones, and a scan fails at the first call into whichever group was missed.
Six Azure resources are needed. Create them in the portal, then give the app permission to use them.
| Create this | What it does here |
|---|---|
| Azure AI Foundry project | Hosts the GPT agent that decides what counts as a dark pattern |
| Azure AI Content Understanding | Reads the screenshots and finds where each button sits |
| Azure AI Content Safety | Blocks hidden instructions planted by the audited site |
| Storage account (1 blob container + 1 table) | Stores screenshots and reports, and tracks scan progress |
| Azure AI Search | Index of GDPR and EU DSA articles the agent searches to confirm a citation |
| Application Insights (optional) | Times each scan stage and records errors |
| Key Vault (optional) | Fallback for local runs if you cannot grant storage permissions |
Then set up the two AI tools. Neither is created by code — you configure
them in their portals using the files in the azure/ folder, which
explains each one in detail:
| Tool | Paste this file into the portal | Then put the ID in |
|---|---|---|
| Content Understanding analyzer | azure/consent-surface-analyzer.json |
AZURE_CU_ANALYZER_ID |
| Foundry agent | azure/agent-instructions.md and azure/agent-output-schema.json |
AZURE_FOUNDRY_AGENT_ID |
The agent also has an azure_ai_search tool over an index of the GDPR and EU
DSA articles. It is attached in the portal, and used only to confirm which
article a proven finding cites — never to decide that a finding exists.
Finally, grant permissions. The app signs in as itself rather than using API keys, so each resource needs a role assigned to it:
| On this resource | Give it this role |
|---|---|
| Storage account | Storage Blob Data Contributor |
| Storage account | Storage Table Data Contributor |
| Foundry project | Azure AI User |
| Content Understanding | Cognitive Services User |
| Azure AI Search | Search Index Data Reader |
| Key Vault (only if using the fallback) | Key Vault Secrets User |
Leaving AZURE_STORAGE_ACCOUNT_NAME blank switches storage to the Key Vault
fallback, which then needs the Key Vault role as well. Filling it in is the
simpler path.
How good are the findings? Score a finished scan with the built-in Azure evaluators:
pip install -r requirements-eval.txt
python -m evaluation.evaluate <run_id>Each finding gets two scores out of 5.
Groundedness — is the reasoning backed by the evidence the agent was given?
Relevance — does the proof answer the question? Results land in backend/evaluation/RESULTS.md, one row per scan, tagged with the agent version. The judge is a language model too, so it scores the same finding differently each time. Each is scored three times and averaged, and every row shows a
Spread — the gap between the highest and lowest verdict. At 1.0 or above,
ignore that row. Compare groundedness across agent versions, never one row on
its own.
Run it after editing azure/agent-instructions.md to check a prompt change
actually helped. It re-scores a stored scan, so it costs a fraction of an audit
and needs no new crawl.
This project uses GPT-5.4-mini deployed at 50,000 TPM (tokens per minute).
Any comparable low-tier reasoning model works just as well — the agent's
behaviour comes from its instructions, not the model. See
azure/agent-instructions.md for exactly what
the agent is told to do.
The 50k TPM is not optional. One audit sends about 28,000 tokens in a single request. On a smaller deployment that one request is bigger than the entire per-minute allowance, so Azure rejects it immediately — and it keeps rejecting everything, even a one-word test message, which looks like a rate limit that never clears. Raising the limit costs nothing extra: you are billed for tokens you actually use, not for the size of the allowance.
backend/.env — copy .env.example and fill it in.
| Variable | What it is |
|---|---|
FOUNDRY_PROJECT_ENDPOINT |
Address of your Foundry project, ending in /api/projects/<project> |
AZURE_FOUNDRY_AGENT_ID |
Name and version of the agent, e.g. dark-pattern-auditor:5 |
CONTENT_UNDERSTANDING_ENDPOINT |
Address of your Content Understanding resource |
AZURE_CU_ANALYZER_ID |
Name you gave the analyzer when creating it |
CONTENT_SAFETY_ENDPOINT |
Address of your Content Safety resource, used for Prompt Shields |
AZURE_STORAGE_ACCOUNT_NAME |
Storage account name. Filled in = sign in as the app; left blank = use the Key Vault fallback |
AZURE_BLOB_CONTAINER |
Container that holds screenshots and reports |
AZURE_TABLE_NAME |
Table that tracks whether a scan is running or finished |
AZURE_CLIENT_ID / AZURE_TENANT_ID / AZURE_CLIENT_SECRET |
Service principal, only needed to run the backend in Docker. Set all three or none |
APPLICATIONINSIGHTS_CONNECTION_STRING |
Application Insights. Blank turns telemetry off |
AZURE_KEY_VAULT_URL |
Key Vault address, only for the local fallback |
STORAGE_CONNECTION_STRING_SECRET |
Name of the secret in Key Vault holding the storage connection string |
MAX_STATES |
Most screens one crawl may visit |
MAX_DEPTH |
How many clicks deep the crawl may go |
WALL_CLOCK_BUDGET_SECONDS |
Time limit for a single crawl |
MAX_ANALYZED_STATES |
Screens sent to Azure AI — the main lever on cost per scan |
- Bounding-box accuracy metric — measure how closely each returned box matches the element actually measured.
- Non-English consent vocabulary — recognise German, French, Spanish and Italian banners, not just English ones.
- Verify consent is actually honoured — compare cookies before and after clicking Reject, and flag trackers set anyway.
- Nagging / Continuous Prompting — revisit a site over several days to catch banners that keep reappearing.
- Multi-step cancellation and unsubscribe flows — audit account-closure journeys, not just cookie consent.
- Mobile viewport auditing — run a second pass at phone size, where consent UI is often worse.
- Pay-or-consent walls — treat "accept tracking or subscribe" as its own pattern with its own evidence rules.
- Scan history and run comparison — list past audits and diff two runs of the same site to see what changed.
- Scheduled monitoring — re-audit a set of sites on a schedule and alert on new findings.
- Report improvements — add an executive summary page, the consent-flow graph, and a JSON export.
- Deploy to Azure Container Apps — run the written deployment pipeline against live infrastructure.
- Durable scan execution — move scans to a queue so a restart cannot abandon one mid-run.
- Cache Content Understanding results — skip re-analysing screens that have not changed.
Contributions are welcome! If you want to contribute, please follow these steps:
- Fork the Repository: Create your own branch from main.
- Create a Feature Branch: git checkout -b feature/AmazingFeature
- Commit your Changes: Write clear commit messages.
- Push to the Branch: git push origin feature/AmazingFeature
- Open a Pull Request: Describe the changes you made and the problem they solve.
If you find a bug or have a feature request, please use the GitHub Issues tab. Include the following in your report:
- A clear title.
- Steps to reproduce the bug.
- Expected vs. actual behavior.
- Screenshots or error logs if you have them.

