Build a Policy From Your Traffic
Writing a Content-Security-Policy by hand means guessing every script, style, image, font, frame and connection your pages load. Discovery replaces the guess with your own traffic. For a week, your visitors’ browsers report everything your site loads, on every page, including the ones behind a login. HeaderHawk then proposes a strict policy, one directive at a time, with the evidence behind each source.
Discovery is on every plan. Owners, admins and members can start and stop it and decide on sources; viewers can read the draft.
1. Start discovery
Section titled “1. Start discovery”Open Policy › Draft in the dashboard, choose the site and press Start discovery. The page shows two headers to add where you set headers today:
- a
Reporting-Endpointsline naming a second endpoint,headerhawk-discovery, beside your site’s own - a
Content-Security-Policy-Report-Onlypolicy that allows nothing:
default-src 'none'; script-src 'report-sample'; style-src 'report-sample';object-src 'none'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'It is report-only, so it blocks nothing. Browsers apply and report every
policy they are sent, so it sits beside any policy you already serve without
changing what that one reports. 'report-sample' asks the browser for the
first 40 characters of each inline script, style and eval, which is how the
draft tells one inline block from another.
Discovery reports go to the site’s endpoint with /discovery appended, and
HeaderHawk keeps them apart from your ordinary reports. They never appear in
Issues or the Explorer, and they never raise an alert, including CSP policy
changed. While discovery is off, that endpoint accepts reports and discards
them.
2. Let it run for a week
Section titled “2. Let it run for a week”A policy that allows nothing makes every page view report every resource on it. To keep that affordable:
- Each browser’s first report of each source is always kept. A source is
a directive and the origin it loaded from, such as
https://cdn.example.netunderscript-src. Chrome, Firefox, Safari and Edge are kept apart, so a source two of them load is seen by both, however quiet the page. - Repeats are sampled at 5%. A repeat only adds to how widely a source is loaded, which a sample shows as well as every report would.
- Only kept reports count toward your monthly allowance. A sampled-out repeat costs nothing.
The page counts the days the run has had. Before seven it recommends a full week, so that a weekly job, a monthly report page or a checkout most visitors never reach has time to be seen. The run keeps going until you stop it.
3. Review the draft
Section titled “3. Review the draft”The draft is one card per directive, its sources ranked by how many reports named them. Each source carries its evidence: the browsers that loaded it, how many reports and pages, up to five example pages, and when it was first and last seen.
How a report becomes a source:
- A URL on the page’s own origin becomes
'self'. - Any other URL becomes its scheme and host, such as
https://cdn.example.net. data:is proposed for images, fonts and media when it was seen, andblob:for images, media, workers and frames. Neither is ever proposed forscript-src.- Browser-extension schemes never become sources. A source whose reports look like extension noise is listed but left out.
- Several subdomains of one site, such as
a.cdn.example.netandb.cdn.example.net, are offered as one*.cdn.example.netsuggestion, left out until you keep it.
A source is in the policy by default once two browser families have loaded it, or once it has about twenty reports. That is the same bar a violation clears before it alerts. Below it, the source is listed with its switch off, because one browser on one visit might be an extension or a one-off.
Turn a switch on to keep a source and off to drop it. The header and its grade change as you do, and your decisions are saved, so the week’s review can happen across several sessions or by several people. A decision outlasts the run: start discovery again later and the sources you kept or dropped keep their answer.
Who each source is, and the ones that look wrong
Section titled “Who each source is, and the ones that look wrong”Each third-party source is named from a catalog of known hosts, built from the open third-party-web dataset: the company, the product when it has several, and what it is for, such as analytics, payments or fonts. A host on your site’s own domain is marked as yours and never looked up. A host on a shared platform, such as a CloudFront distribution or an S3 bucket, is not named after the platform, because anyone can serve files there.
A third-party host the catalog does not know can get a name suggested by an AI model, Claude on Amazon Bedrock, marked Suggested by AI with a sentence on what it does. The model is sent the host name and the directives it was reported under, and nothing about your pages, your visitors or your site. While it is being asked, the source shows Looking up… and the Draft screen checks again every few seconds for about a minute. It answers “unknown” rather than guess, and a name it gives for a company the catalog knows, on a domain the catalog does not list for that company, is dropped, since that is what a look-alike domain gets. A suggestion is only a label: it never changes whether a source is in the policy or flagged. Each host is asked about once and the answer kept for 30 days, for every account, and an account asks about at most 200 new hosts a day.
A source that looks wrong is flagged with the reason, and left out until you keep it or drop it, whatever its evidence:
| Flag | When |
|---|---|
| Arrived late | First seen in the last three days of a week-long run (six of a two-week run), after the rest of the draft had stopped growing. Runs shorter than a week are not checked. |
| Only on payment pages | Every page it was seen on is one of your payment pages. Known payment providers are not flagged, since their scripts load only on the checkout by design. |
| Looks like | A letter or two away from a known host, from another source in the draft, or from your own domain, including look-alike characters from other alphabets. |
| Unknown and rare | Not a known host, and seen on only one page or by only one browser family, though often enough to be in by default. |
A flagged source still counts as one to decide on the Overview, so the review step is not done until someone has kept or dropped it. Leave out drops it and records that you looked. A flag never adds a source to the policy and never removes one from the list. Payment pages are on the Professional and Business plans; without one marked, no source can be flagged for appearing only on them.
Directives the draft sets itself
Section titled “Directives the draft sets itself”Some directives are pinned rather than taken from traffic:
| Directive | Value |
|---|---|
default-src |
'none', so anything not listed is refused |
object-src |
'none', the only safe value |
base-uri |
'none', or 'self' if a page set a <base> element |
frame-ancestors |
'none', or 'self' if a page was framed |
form-action |
'none' if no form was submitted; otherwise its targets, kept or dropped like any source |
A fetch directive nothing was reported under is left out and falls back to
default-src 'none'.
Some of what the run saw cannot be allowed safely by naming a source. Each is a flag above the cards, with its samples and the pages it came from:
- Inline scripts and styles. The draft never contains
'unsafe-inline'. Give each inline script or style a nonce or a hash, or move it to a file. With'strict-dynamic', a nonce on the script that loads the rest is enough. Keywords & Source Expressions covers both. eval. Code built from strings (eval,new Function, a string passed tosetTimeout) is flagged, and the draft never contains'unsafe-eval'.- WebAssembly. Add
'wasm-unsafe-eval'yourself if the site needs it. - Framing and
<base>. No browser names the page that framed yours, or the origin a<base>element pointed at, so add those origins by hand. - Plugins.
object-srcstays'none'. - A scheme where one is not proposed, such as
data:underscript-src.
The grade
Section titled “The grade”The grade beside the header is the one the free scanner would give the policy enforced, with its findings. The draft is served report-only, which the rubric caps, so the grade says what the policy is worth once you enforce it.
4. Deploy the draft report-only
Section titled “4. Deploy the draft report-only”Copy beside the header gives you a Content-Security-Policy-Report-Only header that
reports to the site’s ordinary endpoint, not the discovery one. Open in the
header builder opens the same policy in the free
report header builder,
where you can edit it and get the lines for nginx, Apache, Vercel, Netlify or
Node.js.
Once it is deployed, its violations arrive in Policy › Issues like any other policy’s. A quiet Issues view on the pages discovery saw is the evidence that the draft fits; anything it lists is a source the week did not see, or an inline block that still needs a nonce or a hash.
5. Stop discovery, then enforce
Section titled “5. Stop discovery, then enforce”Press Stop discovery and remove the discovery header and the
headerhawk-discovery endpoint. The draft stays, read from the run’s
reports. Starting discovery again begins a new run from that moment.
When Issues has been quiet for long enough to trust, rename the header from
Content-Security-Policy-Report-Only to Content-Security-Policy. Keep a
report-only copy beside it if you want to try the next change first.