Skip to main content
Glama
README.md
# @mactech/stig-tools

DISA STIG catalog, STIG Viewer `.ckl` writer, CLI and MCP server. 2,029 rules across 15 benchmarks, compiled in; no network needed.

```sh
npx @mactech/stig-tools ckl --benchmark windows2022 --host srv-01 --severity high > srv-01.ckl
```

That file opens in STIG Viewer, imports into eMASS, and is what a POA&M gets written from. Every rule you did not assess comes out `Not_Reviewed`: an unopened checklist asserts nothing, and defaulting to `NotAFinding` would manufacture compliance nobody verified.

The `.ckl` format is STIG Viewer's own XML schema, rigid and undocumented outside the tool. STIG Viewer refuses files whose `STIG_DATA` children are out of order, so the element order in `src/ckl.ts` is load-bearing and tested.

## Install

```sh
npm install @mactech/stig-tools     # library + CLI
npx @mactech/stig-tools --help      # CLI without installing
```

Node 20 or newer. The MCP server additionally needs `@modelcontextprotocol/server` (optional peer dependency).

## CLI

```
stig benchmarks                                   shipped benchmarks and counts
stig search <text> [--benchmark rhel9] [--severity high] [--limit 20] [--json]
stig rule <SV-id>                                 full rule: discussion, check, fix
stig ckl --benchmark <id> --host <name> [--ip] [--fqdn] [--severity] [--status open|not_a_finding|not_applicable]
```

Product ids: `rhel8`, `rhel9`, `ubuntu2204`, `windows11`, `windows2022`, `cisco_ios_router_ndm`, `cisco_ios_router_rtr`, `cisco_ios_switch_l2s`, `cisco_ios_switch_ndm`, `cisco_ios_switch_rtr`, `cisco_ise_nac`, `cisco_ise_ndm`, `cisco_nxos_switch_l2s`, `cisco_nxos_switch_ndm`, `cisco_nxos_switch_rtr`.

## Library

```ts
import { STIG_CATALOG, PRODUCTS, searchStig, findRule, severityToCat } from '@mactech/stig-tools/catalog'
import { buildCkl, summarizeCkl } from '@mactech/stig-tools/ckl'

const page = searchStig('ssh banner', { product: 'rhel9', severity: 'high', limit: 20 })
// { total_matches, offset, returned, next_offset?, rules: StigRule[] }

const rules = STIG_CATALOG.filter((r) => r.product === 'rhel9')
const ckl = buildCkl({
  rules,
  benchmark: rules[0].benchmark,
  asset: { host_name: 'web-01', host_fqdn: 'web-01.example.mil' },
  findings: [{ sv_id: 'SV-257777r991589_rule', status: 'Open', finding_details: 'Banner absent' }],
  exporter: 'Your Tool Name', // recorded in STIG_INFO; defaults to this package's name
})
summarizeCkl(rules, findings) // { Open, NotAFinding, Not_Applicable, Not_Reviewed }
```

`StigRule` fields: `sv_id`, `nist_id` (the CCI, e.g. `CCI-000366`), `severity` (`high`/`medium`/`low` = CAT I/II/III), `title`, `description`, `check_text`, `fix_text`, `product`, `benchmark`, `category`, `automation_level`, `automation_source`.

## MCP server

Mount the tools on any MCP host:

```ts
import { McpServer } from '@modelcontextprotocol/server'
import { registerStigServer, STIG_INSTRUCTIONS } from '@mactech/stig-tools/mcp'

const server = new McpServer({ name: 'stig', version: '1.0.0' }, { instructions: STIG_INSTRUCTIONS })
registerStigServer(server, {
  attributionSuffix: 'Served by Example Corp.',   // optional, appended to every text result
  instrument: (tool, handler) => handler,          // optional telemetry / rate-limit wrapper
  exporter: 'Example Corp',                        // optional, recorded in exported .ckl files
})
// connect your transport (stdio, Streamable HTTP, ...)
```

Tools: `search_stig`, `get_stig_rule`, `list_stig_benchmarks`, `export_stig_checklist`. Resource: `stig://rule/{sv_id}` with completion. Prompt: `harden_host`. The package adds no attribution or telemetry of its own; hosts add theirs through the options.

A hosted instance runs at `https://www.mactechsolutionsllc.com/api/mcp/stig`.

## Data provenance

`data/*.json` is parsed from DISA's official `*_Manual-xccdf.xml` benchmarks, which are works of the United States Government and not subject to copyright (17 U.S.C. § 105). The benchmark version is in each filename (`rhel9_v2r6_controls.json` = RHEL 9 STIG V2R6). The `nist_id` field is DISA's CCI identifier, selected by value from the rule's `<ident>` elements rather than by position, because some Cisco benchmarks list a legacy V-key first. `category` is a coarse grouping inferred from titles; it is remediation metadata, not part of the STIG.

### Refreshing a benchmark

1. Download the STIG ZIP from https://public.cyber.mil/stigs/downloads/ and unzip it locally. Do not commit the ZIP or the XCCDF.
2. Convert:
   ```sh
   node scripts/ingest-stig-xccdf.mjs path/to/U_RHEL_9_V2R7_Manual-xccdf.xml rhel9 data/rhel9_v2r7_controls.json --preserve-categories
   ```
3. Point the `rhel9` row in `src/catalog.ts` `BENCHMARKS` at the new file and delete the old one.
4. `npm test` (update the rule count assertion in `test/catalog.test.ts`), note the version bump in `CHANGELOG.md`.

`--preserve-categories` carries the previous file's `category` values across by `sv_id`; without it categories are re-derived from titles.

## What this is not

- Not a scanner. It does not evaluate a host; it knows what the STIG says and writes the checklist you fill in.
- Not SCAP or OVAL evaluation. `automation_level` is a hint from the check text, not a promise that a tool exists.
- Not the whole STIG library. DISA publishes roughly a thousand benchmarks; this ships fifteen. The ingester makes adding one a download and a command.
- Not a compliance determination. A `.ckl` records what an assessor observed; it does not decide anything.

## License

MIT for the code. The DISA content in `data/` is public domain. See `LICENSE`.