Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions .github/workflows/update-owasp.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Keeps the bundled OWASP secure headers data (json/owasp.json) up to date by
# fetching the latest upstream data on a schedule and opening a PR with the diff.
# This replaces the old runtime fetch, keeping document generation deterministic.

name: Update OWASP Headers

on:
schedule:
# Every Monday at 06:00 UTC
- cron: "0 6 * * 1"
workflow_dispatch:

permissions:
contents: write
pull-requests: write

jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with:
node-version: 22
- name: Fetch latest OWASP headers
run: npm run update:owasp
- name: Create Pull Request
uses: peter-evans/create-pull-request@v8
with:
commit-message: "chore: update bundled OWASP secure headers"
branch: chore/update-owasp-headers
delete-branch: true
title: "chore: update bundled OWASP secure headers"
body: |
Automated update of the bundled OWASP Secure Headers data
(`json/owasp.json`) from the upstream
[www-project-secure-headers](https://github.com/OWASP/www-project-secure-headers)
project.

Source provenance is recorded in `json/owasp.source.json`.
Please review the diff before merging.
labels: dependencies
2 changes: 1 addition & 1 deletion json/owasp.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"last_update_utc": "2026-06-23 16:23:09",
"last_update_utc": "2026-07-19 05:44:10",
"headers": [
{
"name": "Cache-Control",
Expand Down
8 changes: 8 additions & 0 deletions json/owasp.source.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"url": "https://raw.githubusercontent.com/OWASP/www-project-secure-headers/refs/heads/master/ci/headers_add.json",
"repo": "OWASP/www-project-secure-headers",
"path": "ci/headers_add.json",
"branch": "master",
"commit": "0bdb31eeda228b04a2fc712a6aa89105cc3efa84",
"last_update_utc": "2026-07-19 05:44:10"
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@
"serverless openapi"
],
"scripts": {
"test": "mocha --config './test/.mocharc.js'"
"test": "mocha --config './test/.mocharc.js'",
"update:owasp": "node scripts/update-owasp-headers.js"
},
"author": {
"name": "Jared Evans"
Expand Down
107 changes: 107 additions & 0 deletions scripts/update-owasp-headers.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env node
"use strict";

/**
* Refreshes the bundled OWASP Secure Headers data at `json/owasp.json`.
*
* This runs out-of-band (locally via `npm run update:owasp` or on a schedule in
* CI) rather than at document-generation time, so that the plugin never depends
* on a live third-party URL when a user builds their docs. The fetched commit
* SHA is recorded in `json/owasp.source.json` for provenance.
*
* Usage: node scripts/update-owasp-headers.js
*/

const fs = require("fs");
const path = require("path");
const https = require("https");

const OWNER = "OWASP";
const REPO = "www-project-secure-headers";
const BRANCH = "master";
const FILE_PATH = "ci/headers_add.json";

const RAW_URL = `https://raw.githubusercontent.com/${OWNER}/${REPO}/refs/heads/${BRANCH}/${FILE_PATH}`;
const COMMITS_API = `https://api.github.com/repos/${OWNER}/${REPO}/commits?path=${encodeURIComponent(
FILE_PATH
)}&sha=${BRANCH}&per_page=1`;

const OUTPUT_FILE = path.join(__dirname, "..", "json", "owasp.json");
const SOURCE_FILE = path.join(__dirname, "..", "json", "owasp.source.json");

/**
* @param {string} url
* @returns {Promise<string>}
*/
function get(url) {
return new Promise((resolve, reject) => {
https
.get(
url,
{
headers: {
// GitHub's API rejects requests without a User-Agent.
"User-Agent": "serverless-openapi-documenter-owasp-updater",
Accept: "application/vnd.github+json",
},
},
(res) => {
if (res.statusCode !== 200) {
res.resume();
reject(
new Error(`Request to ${url} failed with status ${res.statusCode}`)
);
return;
}

const data = [];
res.on("data", (chunk) => data.push(chunk));
res.on("end", () => resolve(Buffer.concat(data).toString()));
}
)
.on("error", reject);
});
}

async function main() {
console.log(`Fetching OWASP secure headers from ${RAW_URL}`);
const raw = await get(RAW_URL);

// Validate it parses before we overwrite the bundled copy.
const parsed = JSON.parse(raw);
if (!Array.isArray(parsed.headers)) {
throw new Error(
"Unexpected upstream format: expected a `headers` array in the response"
);
}

let sha = null;
try {
const commits = JSON.parse(await get(COMMITS_API));
sha = commits[0]?.sha ?? null;
} catch (err) {
console.warn(`Could not resolve source commit SHA: ${err.message}`);
}

// Re-serialize so the on-disk formatting is stable regardless of upstream
// whitespace, which keeps CI diffs meaningful.
fs.writeFileSync(OUTPUT_FILE, `${JSON.stringify(parsed, null, 2)}\n`);

const source = {
url: RAW_URL,
repo: `${OWNER}/${REPO}`,
path: FILE_PATH,
branch: BRANCH,
commit: sha,
last_update_utc: parsed.last_update_utc ?? null,
};
fs.writeFileSync(SOURCE_FILE, `${JSON.stringify(source, null, 2)}\n`);

console.log(`Wrote ${OUTPUT_FILE}`);
console.log(`Wrote ${SOURCE_FILE} (commit: ${sha ?? "unknown"})`);
}

main().catch((err) => {
console.error(err.message);
process.exit(1);
});
44 changes: 10 additions & 34 deletions src/owasp.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"use strict";

const https = require("https");

const defaultOWASP = require("../json/owasp.json");

/**
Expand Down Expand Up @@ -91,39 +89,17 @@ class OWASP {
};
}

/**
* Populates the OWASP header defaults from the bundled `json/owasp.json`.
*
* The bundled file is kept up to date out-of-band by the `update:owasp`
* script and its scheduled GitHub Action, which fetch the latest upstream
* OWASP Secure Headers project data and open a PR with the diff. This keeps
* document generation deterministic and offline-capable rather than
* depending on a live third-party URL at runtime.
*/
async getLatest() {
const headerJSON = await new Promise((resolve, reject) => {
const req = https
.get(
"https://raw.githubusercontent.com/OWASP/www-project-secure-headers/refs/heads/master/ci/headers_add.json",
(res) => {
let data = [];

if (res.statusCode !== 200) {
resolve(defaultOWASP);
}

res.on("error", (err) => {
resolve(defaultOWASP);
});

res.on("data", (chunk) => {
data.push(chunk);
});

res.on("end", () => {
resolve(JSON.parse(Buffer.concat(data).toString()));
});
}
)
.on("error", (err) => {
resolve(defaultOWASP);
});

req.end();
});

this.populateDefaults(headerJSON);
this.populateDefaults(defaultOWASP);
}

/**
Expand Down
31 changes: 7 additions & 24 deletions test/unit/owasp.spec.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"use strict";

const expect = require("chai").expect;
const nock = require("nock");

const owasp = require("../../src/owasp");

Expand All @@ -10,11 +9,7 @@ const newOWASPJSON = require("../json/newOWASP.json");

describe(`owasp`, function () {
describe(`getLatest`, function () {
it(`populates the defaults from the included OWASP release when the online version can not be reached`, async function () {
nock("https://raw.githubusercontent.com")
.get("/OWASP/www-project-secure-headers/refs/heads/master/ci/headers_add.json")
.reply(404, {});

it(`populates the defaults from the bundled OWASP release`, async function () {
await owasp.getLatest().catch((err) => {
console.error(err);
expect(err).to.be.undefined;
Expand All @@ -31,16 +26,11 @@ describe(`owasp`, function () {
).to.be.equal(permissionsPolicyDefault[0].value);
expect(Object.keys(owasp.DEFAULT_OWASP_HEADERS).length).to.be.equal(13);
});
});

it(`populates the defaults with information from a new OWASP release`, async function () {
nock("https://raw.githubusercontent.com")
.get("/OWASP/www-project-secure-headers/refs/heads/master/ci/headers_add.json")
.reply(200, newOWASPJSON);

await owasp.getLatest().catch((err) => {
console.error(err);
expect(err).to.be.undefined;
});
describe(`populateDefaults`, function () {
it(`populates the defaults with information from an OWASP release`, function () {
owasp.populateDefaults(newOWASPJSON);

expect(
owasp.DEFAULT_OWASP_HEADERS["Cross-Origin-Embedder-Policy"]
Expand All @@ -55,18 +45,11 @@ describe(`owasp`, function () {
expect(Object.keys(owasp.DEFAULT_OWASP_HEADERS).length).to.be.equal(13);
});

it(`adds any properties contained in a new release`, async function () {
it(`adds any properties contained in a new release`, function () {
const newOWASPJSONAdded = structuredClone(newOWASPJSON);
newOWASPJSONAdded.headers.push({ name: "x-added", value: "true" });

nock("https://raw.githubusercontent.com")
.get("/OWASP/www-project-secure-headers/refs/heads/master/ci/headers_add.json")
.reply(200, newOWASPJSONAdded);

await owasp.getLatest().catch((err) => {
console.error(err);
expect(err).to.be.undefined;
});
owasp.populateDefaults(newOWASPJSONAdded);

expect(owasp.DEFAULT_OWASP_HEADERS).to.have.property("x-added");
expect(owasp.DEFAULT_OWASP_HEADERS["x-added"]).to.have.property("schema");
Expand Down
Loading