diff --git a/docs/banner.svg b/docs/banner.svg
index b36ee38..9716b88 100644
--- a/docs/banner.svg
+++ b/docs/banner.svg
@@ -1,4 +1,5 @@
\ No newline at end of file
diff --git a/docs/index.html b/docs/index.html
index cd15cf7..b3df0cb 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -1859,7 +1859,8 @@
AI Security Standards Crosswalk
{
icon: '\uD83D\uDD0D',
title: 'Explore risks & controls',
- desc: 'Search 41 OWASP entries, filter by severity, click any entry to see all 25 framework mappings.',
+ desc: 'Search ' + DATA.length + ' OWASP entries, filter by severity, click any entry to see its mappings across all ' +
+ FRAMEWORKS.length + ' frameworks.',
link: '#/explorer',
cta: 'Open explorer \u2192',
who: 'Security engineers, architects, auditors'
@@ -5654,7 +5655,7 @@
AI Security Standards Crosswalk
page.appendChild(el('h2', {}, 'Submit a Standard'));
page.appendChild(el('p', { style: 'color:var(--text-secondary);margin-bottom:20px;' },
- 'Submit a security framework for automated mapping to all 41 OWASP GenAI entries. ' +
+ 'Submit a security framework for automated mapping to all ' + DATA.length + ' OWASP GenAI entries. ' +
'Paste your framework controls as JSON below. Our classifier will propose mappings and open a PR for review.'));
// Example template
diff --git a/docs/og-image.svg b/docs/og-image.svg
index e8143e3..6137dc2 100644
--- a/docs/og-image.svg
+++ b/docs/og-image.svg
@@ -1,4 +1,5 @@
\ No newline at end of file
+ genai-security-project.github.io/crosswalk
+ OWASP GenAI Data Security Initiative · CC BY-SA 4.0
+
diff --git a/scripts/render-stats.mjs b/scripts/render-stats.mjs
index 8f9b791..7f5c2dd 100644
--- a/scripts/render-stats.mjs
+++ b/scripts/render-stats.mjs
@@ -1,21 +1,23 @@
#!/usr/bin/env node
/**
- * render-stats.mjs — Render generated counts into README marker regions.
+ * render-stats.mjs — Render generated counts into marker regions.
*
- * Every headline number in the README lives between a pair of HTML comments:
+ * Every headline number in the README, and in the two brand images that repeat
+ * those numbers (the social card and the banner), lives between a pair of
+ * comments. HTML and SVG share the comment syntax:
*
* 23
*
* The text between the markers is replaced from `data/stats.json`; everything
- * else in the file is left byte-identical. `npm run stats:check` re-renders and
- * asserts `git diff --exit-code`, so a hand-edited count fails CI.
+ * else in each file is left byte-identical. `npm run stats:check` re-renders and
+ * fails if any target would change, so a hand-edited count fails CI.
*
- * Adding a number to the README means wrapping it in a marker and adding the
- * key to KEYS below — never hand-maintaining the digits.
+ * Adding a number means wrapping it in a marker and adding the key to KEYS
+ * below — never hand-maintaining the digits.
*
* Usage:
- * node scripts/render-stats.mjs # rewrite README.md in place
- * node scripts/render-stats.mjs --check # exit 1 if the README is stale
+ * node scripts/render-stats.mjs # rewrite every target in place
+ * node scripts/render-stats.mjs --check # exit 1 if any target is stale
*/
import fs from 'node:fs';
@@ -23,7 +25,8 @@ import path from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
-const README = path.join(ROOT, 'README.md');
+// The badge block is Markdown, so it belongs in README.md only.
+const TARGETS = ['README.md', 'docs/og-image.svg', 'docs/banner.svg'];
const STATS = path.join(ROOT, 'data', 'stats.json');
const CHECK = process.argv.includes('--check');
@@ -107,26 +110,33 @@ function render(src) {
* working copy as stale. The repo is LF-canonical in the index either way, so all
* work happens in LF and git converts on checkout.
*/
-const before = fs.readFileSync(README, 'utf8').replace(/\r\n/g, '\n');
-const { out, seen, unknown } = render(before);
+let failed = false;
-if (unknown.length) {
- console.error(`✗ unknown stats key(s) in README: ${[...new Set(unknown)].join(', ')}`);
- console.error(` known keys: ${Object.keys(KEYS).join(', ')}`);
- process.exit(1);
-}
+for (const rel of TARGETS) {
+ const file = path.join(ROOT, rel);
+ const before = fs.readFileSync(file, 'utf8').replace(/\r\n/g, '\n');
+ const { out, seen, unknown } = render(before);
-if (CHECK) {
- if (out !== before) {
- console.error('✗ README.md is stale — run `npm run stats`');
- process.exit(1);
+ if (unknown.length) {
+ console.error(`✗ unknown stats key(s) in ${rel}: ${[...new Set(unknown)].join(', ')}`);
+ console.error(` known keys: ${Object.keys(KEYS).join(', ')}`);
+ failed = true;
+ continue;
}
- console.log(`✓ README.md is current (${seen.size} marker keys in use)`);
-} else {
- if (out !== before) {
- fs.writeFileSync(README, out, 'utf8');
- console.log(`Rewritten README.md (${seen.size} marker keys)`);
+
+ if (CHECK) {
+ if (out !== before) {
+ console.error(`✗ ${rel} is stale — run \`npm run stats\``);
+ failed = true;
+ } else {
+ console.log(`✓ ${rel} is current (${seen.size} marker keys in use)`);
+ }
+ } else if (out !== before) {
+ fs.writeFileSync(file, out, 'utf8');
+ console.log(`Rewritten ${rel} (${seen.size} marker keys)`);
} else {
- console.log(`README.md already current (${seen.size} marker keys)`);
+ console.log(`${rel} already current (${seen.size} marker keys)`);
}
}
+
+if (failed) process.exit(1);
diff --git a/scripts/validate.js b/scripts/validate.js
index 941ff64..93ddaed 100644
--- a/scripts/validate.js
+++ b/scripts/validate.js
@@ -671,9 +671,6 @@ const ATTRIBUTION_EXEMPT = [
'GOVERNANCE.md',
// Historical record; past releases are not rewritten.
'CHANGELOG.md',
- // Brand assets are frozen (C2) — the webapp's logo/social images.
- 'docs/og-image.svg',
- 'docs/banner.svg',
];
const ATTRIBUTION_SKIP_DIRS = ['node_modules', '.git', 'reports', 'dist', 'coverage'];
diff --git a/scripts/webapp.test.mjs b/scripts/webapp.test.mjs
index 370ef7d..9439918 100644
--- a/scripts/webapp.test.mjs
+++ b/scripts/webapp.test.mjs
@@ -161,6 +161,20 @@ test('the home page counts mapped frameworks only', () => {
assert.equal(cards['Registry Controls'], String(controls));
});
+// The landing and Submit pages used to hard-code "41 entries" and "25
+// frameworks". Counts in page copy must come from the loaded data.
+test('page copy states the live entry and framework counts', () => {
+ const { window } = dom;
+ const entries = window.CROSSWALK_DATA.length;
+ const frameworks = new Set(window.CROSSWALK_DATA.flatMap((e) => e.mappings.map((m) => m.framework))).size;
+
+ const home = visit('#/').textContent;
+ assert.match(home, new RegExp(`Search ${entries} OWASP entries`));
+ assert.match(home, new RegExp(`across all ${frameworks} frameworks`));
+
+ assert.match(visit('#/submit').textContent, new RegExp(`all ${entries} OWASP GenAI entries`));
+});
+
// The About page is the one place in the webapp allowed to carry the creator
// credit, and this file is not — so the test checks that the section renders
// with content, never the name itself.