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
33 changes: 33 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ on:
- 'README.md'
- 'scripts/validate.js'
- 'scripts/generate.js'
- 'scripts/*.test.mjs'
- 'package.json'
- '.github/workflows/validate.yml'
pull_request:
branches: [main]
Expand All @@ -28,6 +30,8 @@ on:
- 'README.md'
- 'scripts/validate.js'
- 'scripts/generate.js'
- 'scripts/*.test.mjs'
- 'package.json'
- '.github/workflows/validate.yml'

# Both jobs only read the tree. Without this they inherit the repository
Expand Down Expand Up @@ -112,3 +116,32 @@ jobs:
# here rather than shipping.
- name: Assert generated output is current
run: git diff --exit-code -- data/entries docs/data.js docs/incidents.js

unit-tests:
name: Unit tests
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
with:
persist-credentials: false

- name: Use Node.js
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: '20'
cache: npm

# ajv is the only runtime dependency the suite needs, and it is a
# devDependency. --ignore-scripts because nothing here needs a postinstall.
- name: Install dependencies
run: npm ci --ignore-scripts

# Covers stats.js, generate.js, the three report CLIs, the framework
# ingest path, and schema validation of the OSCAL and STIX exports.
- name: Run the test suite
run: npm run test:scripts

# The suite re-runs the generator to prove it is deterministic. If that
# left anything behind, the tree must still be clean.
- name: Assert the tests left no changes behind
run: git diff --exit-code
1 change: 1 addition & 0 deletions data/framework-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
"controls": {
"type": "array",
"description": "Complete inventory of controls/clauses/requirements",
"minItems": 1,
"items": {
"type": "object",
"required": [
Expand Down
61 changes: 61 additions & 0 deletions data/schemas/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<!--
OWASP GenAI Crosswalk
File : data/schemas/README.md
Purpose : Why the export schemas here are subsets, and what they do and do not prove
Version : 2026-Q3
License : CC BY-SA 4.0
-->

# Export schemas

The crosswalk emits three machine-readable formats that other tools consume:

| Format | Emitted by | Schema here |
|---|---|---|
| OSCAL 1.1.2 Component Definition | `compliance-report.js --format oscal` | `oscal-component-definition.subset.json` |
| OSCAL 1.1.2 Catalog | `compliance-report.js --format oscal-catalog` | `oscal-catalog.subset.json` |
| STIX 2.1 Bundle | `incidents-report.js --format stix` | `stix-bundle.subset.json` |

`scripts/exports.test.mjs` validates every emitted document against these on
each `node --test` run, and `npm run ci` includes it.

## These are subsets, and the filename says so

They are **not** the NIST and OASIS schemas. They encode the structural rules
this project's output has to satisfy — required members, id formats, the
`spec_version` and `oscal-version` constants, UUID and timestamp shapes,
`additionalProperties` where the spec closes an object — and they are written
here, in this repository, by this project.

Passing them proves the export has not silently lost its shape. It does not
prove full OSCAL or STIX conformance, and no sentence in this repository should
claim it does.

## Why not the upstream schemas

Two options were considered and both cost more than they return here:

- **Fetch at CI time.** Makes every build depend on a third-party host being up
and on a schema that can change without warning. A red build that means
"NIST changed a description" is a build people learn to ignore.
- **Vendor the upstream files.** The OSCAL complete schema is over a megabyte of
JSON this project does not control and cannot meaningfully review on update.

A small schema that is read, understood and owned catches the regressions that
actually happen — a renamed member, a dropped `spec_version`, a malformed
UUID — and it does so without pretending to an authority it does not have.

For genuine conformance, run the emitted file through the upstream validators:

```bash
# OSCAL — https://github.com/usnistgov/OSCAL
node scripts/compliance-report.js --framework "NIST AI RMF 1.0" --format oscal --stdout > oscal.json

# STIX 2.1 — https://github.com/oasis-open/cti-stix-validator
node scripts/incidents-report.js --format stix --stdout > stix.json
```

---

*Part of the [OWASP GenAI Crosswalk](https://github.com/GenAI-Security-Project/crosswalk) —
maintained by the [OWASP GenAI Data Security Initiative](https://genai.owasp.org)*
74 changes: 74 additions & 0 deletions data/schemas/oscal-catalog.subset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/GenAI-Security-Project/crosswalk/blob/main/data/schemas/oscal-catalog.subset.json",
"title": "OSCAL 1.1.2 Catalog — structural subset",
"description": "A SUBSET of the NIST OSCAL 1.1.2 catalog model, written and owned by this project. It encodes the structure compliance-report.js --format oscal-catalog must emit. Passing proves the export has not lost its shape; it does not prove OSCAL conformance. See data/schemas/README.md.",
"type": "object",
"required": ["catalog"],
"additionalProperties": false,
"properties": {
"catalog": {
"type": "object",
"required": ["uuid", "metadata"],
"properties": {
"uuid": { "$ref": "#/definitions/uuid" },
"metadata": { "$ref": "#/definitions/metadata" },
"groups": {
"type": "array",
"items": { "$ref": "#/definitions/group" }
},
"controls": {
"type": "array",
"items": { "$ref": "#/definitions/control" }
},
"back-matter": { "type": "object" }
}
}
},
"definitions": {
"uuid": {
"type": "string",
"pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"
},
"metadata": {
"type": "object",
"required": ["title", "last-modified", "version", "oscal-version"],
"properties": {
"title": { "type": "string", "minLength": 1 },
"last-modified": { "type": "string", "format": "date-time" },
"version": { "type": "string", "minLength": 1 },
"oscal-version": { "const": "1.1.2" },
"roles": { "type": "array" },
"parties": { "type": "array" },
"responsible-parties": { "type": "array" }
}
},
"group": {
"type": "object",
"required": ["id", "title"],
"properties": {
"id": { "$ref": "#/definitions/tokenId" },
"title": { "type": "string", "minLength": 1 },
"controls": { "type": "array", "items": { "$ref": "#/definitions/control" } },
"groups": { "type": "array", "items": { "$ref": "#/definitions/group" } }
}
},
"control": {
"type": "object",
"required": ["id", "title"],
"properties": {
"id": { "$ref": "#/definitions/tokenId" },
"title": { "type": "string", "minLength": 1 },
"props": { "type": "array", "items": { "type": "object", "required": ["name", "value"] } },
"links": { "type": "array", "items": { "type": "object", "required": ["href"] } },
"parts": { "type": "array" },
"controls": { "type": "array", "items": { "$ref": "#/definitions/control" } }
}
},
"tokenId": {
"type": "string",
"description": "OSCAL identifiers are NCName-like tokens: they must not start with a digit and must not contain whitespace. Control ids taken straight from a framework often do both, so this is the check most likely to catch a real regression.",
"pattern": "^[A-Za-z_][A-Za-z0-9._~:-]*$"
}
}
}
128 changes: 128 additions & 0 deletions data/schemas/oscal-component-definition.subset.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://github.com/GenAI-Security-Project/crosswalk/blob/main/data/schemas/oscal-component-definition.subset.json",
"title": "OSCAL 1.1.2 Component Definition — structural subset",
"description": "A SUBSET of the NIST OSCAL 1.1.2 component-definition model, written and owned by this project. It encodes the structure compliance-report.js --format oscal must emit. Passing proves the export has not lost its shape; it does not prove OSCAL conformance. See data/schemas/README.md.",
"type": "object",
"required": ["component-definition"],
"additionalProperties": false,
"properties": {
"component-definition": {
"type": "object",
"required": ["uuid", "metadata", "components"],
"properties": {
"uuid": { "$ref": "#/definitions/uuid" },
"metadata": { "$ref": "#/definitions/metadata" },
"components": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#/definitions/component" }
},
"back-matter": { "type": "object" }
}
}
},
"definitions": {
"uuid": {
"type": "string",
"pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$"
},
"metadata": {
"type": "object",
"required": ["title", "last-modified", "version", "oscal-version"],
"properties": {
"title": { "type": "string", "minLength": 1 },
"last-modified": { "type": "string", "format": "date-time" },
"version": { "type": "string", "minLength": 1 },
"oscal-version": {
"type": "string",
"description": "Pinned. A silent bump here changes what downstream tooling will accept.",
"const": "1.1.2"
},
"roles": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "title"],
"properties": { "id": { "type": "string" }, "title": { "type": "string" } }
}
},
"parties": {
"type": "array",
"items": {
"type": "object",
"required": ["uuid", "type", "name"],
"properties": {
"uuid": { "$ref": "#/definitions/uuid" },
"type": { "enum": ["person", "organization"] },
"name": { "type": "string", "minLength": 1 }
}
}
},
"responsible-parties": { "type": "array" }
}
},
"component": {
"type": "object",
"required": ["uuid", "type", "title", "description"],
"properties": {
"uuid": { "$ref": "#/definitions/uuid" },
"type": { "type": "string", "minLength": 1 },
"title": { "type": "string", "minLength": 1 },
"description": { "type": "string", "minLength": 1 },
"control-implementations": {
"type": "array",
"items": {
"type": "object",
"required": ["uuid", "source", "description", "implemented-requirements"],
"properties": {
"uuid": { "$ref": "#/definitions/uuid" },
"source": { "type": "string", "minLength": 1 },
"description": { "type": "string" },
"implemented-requirements": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["uuid", "control-id", "description"],
"properties": {
"uuid": { "$ref": "#/definitions/uuid" },
"control-id": { "type": "string", "minLength": 1 },
"description": { "type": "string" },
"props": { "$ref": "#/definitions/props" },
"links": { "$ref": "#/definitions/links" }
}
}
}
}
}
}
}
},
"props": {
"type": "array",
"items": {
"type": "object",
"required": ["name", "value"],
"properties": {
"name": { "type": "string", "minLength": 1 },
"value": { "type": "string" },
"ns": { "type": "string" },
"class": { "type": "string" }
}
}
},
"links": {
"type": "array",
"items": {
"type": "object",
"required": ["href"],
"properties": {
"href": { "type": "string", "minLength": 1 },
"rel": { "type": "string" },
"text": { "type": "string" }
}
}
}
}
}
Loading
Loading