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
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
# CI invokes these same targets rather than re-implementing them in YAML, which is
# what prevents a gate silently degrading into a warning. There is no warning tier.

.PHONY: check check-licensing check-public-claims check-deb-ordering check-reproducible check-sbom check-tests check-python-floor check-packaging check-licensing check-public-claims check-privacy check-docs-truth check-current-state check-headers check-docs check-scope check-shell check-refs check-paths check-index check-freeze check-vectors check-vectors-negative check-vectors-crossversion check-gate-coverage check-falsifiable help
.PHONY: check check-native-catalog check-licensing check-public-claims check-deb-ordering check-reproducible check-sbom check-tests check-python-floor check-packaging check-native-catalog check-licensing check-public-claims check-privacy check-docs-truth check-current-state check-headers check-docs check-scope check-shell check-refs check-paths check-index check-freeze check-vectors check-vectors-negative check-vectors-crossversion check-gate-coverage check-falsifiable help

check: check-scope check-headers check-python-floor check-packaging check-licensing check-public-claims check-privacy check-docs-truth check-current-state check-shell check-refs check-paths check-index check-freeze check-vectors check-vectors-negative check-vectors-crossversion check-tests check-docs
check: check-scope check-headers check-python-floor check-packaging check-native-catalog check-licensing check-public-claims check-privacy check-docs-truth check-current-state check-shell check-refs check-paths check-index check-freeze check-vectors check-vectors-negative check-vectors-crossversion check-tests check-docs
@echo "make check: all gates passed"

## check-scope D-96: no product implementation before architecture freeze
Expand Down Expand Up @@ -85,6 +85,10 @@ check-python-floor:
check-packaging:
@python3 scripts/ci/check_packaging.py

## check-native-catalog D-111: the control catalog is ISEDRAF's own, and stays that way
check-native-catalog:
@python3 scripts/ci/check_native_catalog.py

## check-licensing D-84/D-90: MPL covers what we own; unknown licensing is not distributable
check-licensing:
@python3 scripts/ci/check_licensing.py
Expand Down
5 changes: 3 additions & 2 deletions docs/CURRENT_STATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ Designed, not built. No part of this runs.
- `framework_mapping_packs` — design only - no pack loader, no manifest reader, no entitlement mechanism and no signing exists; no third-party mapping is licensed, reviewed or bundled (design: `docs/licensing/FRAMEWORK_PACK_ARCHITECTURE.md`)
- `journald_recording`
- `mounts`
- `native_control_catalog` — D-111 freezes the invariant and the ISE-* namespace: 14 families, ISE-IDENT reserved and not in use. NO native criterion is authored yet - they arrive in W1-D. make check-native-catalog enforces that the registry and the catalog document agree, that criteria hold to the namespace, that no criterion is derived from a framework, and that no production module is named after a provider (design: `docs/architecture/NATIVE_CONTROL_CATALOG.md`)
- `pam`
- `report_pdf`
- `services`
Expand Down Expand Up @@ -104,8 +105,8 @@ Not asserted. Each number is counted at generation time.

| | |
|---|---|
| Gates | 17 |
| Falsification injections | 70 |
| Gates | 18 |
| Falsification injections | 73 |
| Golden vector cases | 15 |
| Frozen artifacts | 7 |
| Test files | 3 |
Expand Down
147 changes: 147 additions & 0 deletions docs/architecture/NATIVE_CONTROL_CATALOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
<!--
SPDX-License-Identifier: MPL-2.0
SPDX-FileCopyrightText: Copyright (c) 2026 Antonios Voulvoulis / ITCMS
-->
# ISEDRAF Native Control Catalog

Implements: D-78, D-79, D-82, D-84, D-111

**Architecture invariant, owner decision 2026-09-19 (`D-111`), frozen before W1-D is designed.**

> The ISEDRAF native control catalog is authored first. Framework authority is mapped second, and
> only where licensing permits. **An external framework is never the source of a control.**

## The layering

```text
LINUX FACT
↓
ISEDRAF NATIVE CONTROL
↓
ISEDRAF RESULT / EVIDENCE
↓
OPTIONAL AUTHORITY MAPPING
├── open / public mapping
├── provider-licensed mapping
└── no mapping
```

The arrow runs one way. A framework requirement never defines how a collector is implemented or what
a criterion says, and removing every mapping leaves the catalog exactly as useful.

## Three ownerships, kept apart

| | |
|---|---|
| **Control ownership** | ISEDRAF |
| **Evidence ownership** | the customer, and the host observed |
| **Framework mapping rights** | framework- and provider-specific |

Separating these is the whole point. The first two are ours and the customer's and carry no
third-party licensing question. Only the third does, and it is isolated so that a licensing problem
in one framework cannot reach the engine, the evidence, or any other framework.

## What ISEDRAF claims about authorship — and what it does not

> ISEDRAF independently authors technical host-assurance criteria based on Linux system behaviour,
> security engineering principles, and sources whose reuse rights permit that use. External framework
> content is not required to define those criteria.

That is the claim, and it is deliberately narrower than *"nothing could ever forbid a control"*. This
project does not assert that no patent, contract, trademark, copyright, database right or other
restriction could exist anywhere in the world. It asserts what it can support: the criteria are
independently authored, and they do not depend on licensed framework content.

The distinction matters in a legal document. An absolute claim is the kind of sentence that is
cost-free to write and expensive to defend.

## Namespace

Authority: `scripts/ci/native_controls.json`. `make check-native-catalog` requires this document and
that file to agree, because two authorities that can disagree are how a namespace drifts.

| Family | Domain |
|---|---|
| `ISE-ASSET-*` | asset and host inventory |
| `ISE-SW-*` | software and package state |
| `ISE-ACCOUNT-*` | user, service and administrative accounts |
| `ISE-AUTH-*` | authentication |
| `ISE-PRIV-*` | sudo and privilege |
| `ISE-SSH-*` | SSH posture |
| `ISE-SVC-*` | service exposure and state |
| `ISE-KERNEL-*` | sysctl and kernel security |
| `ISE-LOG-*` | audit and logging |
| `ISE-TIME-*` | clock and time synchronization |
| `ISE-CRYPTO-*` | cryptographic posture |
| `ISE-STORAGE-*` | filesystem, storage and mount security |
| `ISE-NET-*` | network configuration |
| `ISE-UPDATE-*` | update and support posture |

**`ISE-IDENT-*` is reserved and not in use.** An earlier draft used it for accounts. Host *identity*
(`machine-id`, `host_id`) and user *accounts* are different domains, and one prefix meaning both
would have been a permanent source of confusion. Accounts are `ISE-ACCOUNT-*`; the reservation stands
so the earlier draft resolves to this explanation rather than to silence.

## What a criterion contains

Required: `criterion_id` · `purpose` · `facts_required` · `dimensions` (declared / resolved / active,
where applicable) · `evaluation_semantics` · `applicability` · `limitations` · `evidence_pointers` ·
`version`. Optional: `remediation_guidance`.

Result states: `PASS` · `FAIL` · `PARTIAL` · `NOT_EVALUATED`.

Every field is ISEDRAF-authored. No framework identifier, title, description or safeguard text
appears in a criterion — a mapping is a separate object in a separate layer.

### Illustrative shape

```text
ISE-SSH-001
purpose Determine the effective SSH root-login posture.
facts effective PermitRootLogin value · configuration source · resolution status
states PASS / FAIL / PARTIAL / NOT_EVALUATED
evidence the exact normalized observed state
```

Nobody's permission is required to write a Linux security criterion about effective SSH
configuration. The licensing question begins one layer later, at *"this corresponds to provider X
control Y"* — which can involve another party's identifiers, taxonomy, titles, descriptions,
profiles, selection and arrangement, trademarks or proprietary mapping data. That is exactly why it
is isolated.

## Build order

**Stage 1 — the catalog.** Author the ISEDRAF control universe. No CIS, ISO, SCF or anything else is
required anywhere in it. *(W1-D and later. Nothing is authored today.)*

**Stage 2 — open authorities.** For sources whose exact reuse rights are verified, add mappings. The
ISEDRAF criterion remains authoritative for the engine even here.

**Stage 3 — restricted authorities.** An encrypted licensed mapping pack plus a provider entitlement.

```text
ISEDRAF control always available
ISEDRAF evidence always available
provider mapping entitlement required
provider report view entitlement required
provider content provider controlled
```

## Why this is frozen before W1-D

It means the technical engine can be finished without waiting for anyone.

```text
provider says yes later → existing controls + licensed mapping pack
provider says no → existing controls
```

Either way there is no collector rewrite and nothing is lost — which is what
*measure once, map everywhere, fix only the delta* has to mean in practice if it is going to survive
contact with a licensing negotiation.

## Current state

**No native criterion is authored.** The namespace is frozen; the catalog is empty; no mapping of any
kind exists. `scripts/ci/native_controls.json` says so, and the gate does not treat an empty catalog
as a pass by omission.
7 changes: 6 additions & 1 deletion docs/licensing/FRAMEWORK_MAPPING_POLICY.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ ISEDRAF EVIDENCE / RESULT observed, with its limits stated
OPTIONAL FRAMEWORK MAPPING a downstream overlay, if one is licensed
```

The arrow never runs the other way. A framework requirement does not define how a collector is
The arrow never runs the other way. This is frozen as an architecture invariant in
[`NATIVE_CONTROL_CATALOG.md`](../architecture/NATIVE_CONTROL_CATALOG.md) (`D-111`), which fixes the
`ISE-*` namespace and separates three ownerships: **control ownership** is ISEDRAF's, **evidence
ownership** is the customer's and the observed host's, and only **framework mapping rights** are
framework-specific. The practical effect is that the technical engine is completed without waiting
for any provider — a later agreement adds a mapping pack, and a refusal costs nothing already built. A framework requirement does not define how a collector is
implemented, and there is no `cis_collector.py`, `iso_collector.py` or `scf_collector.py` — there is
a users collector, an SSH collector, a logging collector. One host collection can then serve many
mappings.
Expand Down
129 changes: 129 additions & 0 deletions scripts/ci/check_native_catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# =============================================================================
# ISEDRAF — Linux Host Assurance, State Delta & Evidence Bridge (codename)
# =============================================================================
# SPDX-License-Identifier: MPL-2.0
# SPDX-FileCopyrightText: Copyright (c) 2026 Antonios Voulvoulis / ITCMS <contact@itcms.gr>
#
# Purpose: The native control catalog is ISEDRAF's own, and stays that way.
# Implements: D-78, D-79, D-84, D-111, GOV-002
#
# D-111 freezes the layering: LINUX FACT -> NATIVE CONTROL -> EVIDENCE -> OPTIONAL MAPPING.
# An invariant with no gate is a sentence, and this project has learned what those are
# worth. Four things are checked, and the first is the one that keeps the namespace honest.
#
# 1. The registry and the architecture document must AGREE on the families. Two
# authorities that can disagree are how a namespace drifts - the same defect class as
# the rpm spec staging two documents while build.sh staged four.
# 2. Every criterion ID matches the frozen namespace pattern and a declared family.
# 3. No criterion may cite a framework as its source. The control is ours or it is not a
# native control.
# 4. No production module may be named after a framework provider. There is no
# cis_collector.py, and the gate is what makes that a fact rather than an intention.
#
# meta:type="ci-gate"
# meta:owner="Antonios Voulvoulis / ITCMS"
# meta:stability="EXPERIMENTAL"
# meta:privilege="unprivileged"
# meta:mutates="none"
# meta:binaries="git,python3"
# =============================================================================

"""usage: check_native_catalog.py"""
import json
import pathlib
import re
import subprocess
import sys

ROOT = pathlib.Path(subprocess.check_output(
["git", "rev-parse", "--show-toplevel"], text=True).strip())
REG = json.loads((ROOT / "scripts" / "ci" / "native_controls.json").read_text())
DOC = ROOT / "docs" / "architecture" / "NATIVE_CONTROL_CATALOG.md"
FAIL = []


def bad(msg):
FAIL.append(msg)
print(" FAIL %s" % msg)


print("--- native control catalog (D-111) ---")

families = REG["families"]
# Z-18: count the input before judging it. A namespace with no families is not a pass.
if not families:
bad("the registry declares no control families")
sys.exit(1)

# --- 1. the registry and the document agree ------------------------------------------
if not DOC.exists():
bad("the catalog document is missing: %s" % DOC.relative_to(ROOT))
else:
text = DOC.read_text(encoding="utf-8")
in_doc = set(re.findall(r"`(ISE-[A-Z]+)-\*`", text))
declared = set(families)
reserved = set(REG.get("reserved_families", {}))
for f in sorted(declared - in_doc):
bad("%s is in the registry and not in the catalog document" % f)
for f in sorted(in_doc - declared - reserved):
bad("%s is in the catalog document and not in the registry" % f)
for f in sorted(reserved):
if f not in text:
bad("%s is reserved but the document does not explain why" % f)
if not (declared - in_doc) and not (in_doc - declared - reserved):
print(" OK %d families: registry and catalog document agree" % len(declared))

# --- 2. criterion IDs hold to the frozen namespace -----------------------------------
pattern = re.compile(REG["namespace_pattern"])
required = set(REG["required_criterion_fields"])
seen = set()
for c in REG["criteria"]:
cid = c.get("criterion_id", "<missing>")
if not pattern.match(cid or ""):
bad("%r does not match the frozen namespace %s" % (cid, REG["namespace_pattern"]))
continue
if cid in seen:
bad("%s is defined more than once" % cid)
seen.add(cid)
fam = cid.rsplit("-", 1)[0]
if fam not in families:
bad("%s belongs to family %s, which is not declared" % (cid, fam))
missing = required - set(c)
if missing:
bad("%s is missing required field(s): %s" % (cid, ", ".join(sorted(missing))))

# --- 3. a native control is not derived from a framework -----------------------------
RESTRICTED = re.compile(
r"\b(CIS|ISO[/ ]?IEC|ISO\s*27\d{3}|SCF|HITRUST|COBIT|PCI[- ]?DSS|NIS2|DORA|CCM)\b")
for c in REG["criteria"]:
blob = json.dumps(c)
m = RESTRICTED.search(blob)
if m:
bad("%s names %s in the criterion itself. A native control is authored from Linux "
"behaviour; a framework reference belongs in a mapping, which is a separate "
"layer." % (c.get("criterion_id", "<unnamed>"), m.group(0)))
if REG["criteria"]:
print(" OK %d criteria: namespace, fields and independence hold" % len(seen))
else:
print(" OK catalog is empty — no native criterion is authored yet (W1-D), and the "
"registry says so rather than implying otherwise")

# --- 4. no production module named after a framework provider ------------------------
PROVIDER_NAME = re.compile(r"(^|[_/-])(cis|iso27\d*|iso|scf|hitrust|cobit|pci|nist|ccm)"
r"([_/-]|\.py$)", re.I)
tracked = subprocess.check_output(["git", "ls-files", "lib/"], cwd=str(ROOT), text=True).split()
if not tracked:
bad("no production files found under lib/ — nothing was checked")
else:
offenders = [p for p in tracked if PROVIDER_NAME.search(pathlib.Path(p).name)]
for p in offenders:
bad("%s is named after a framework provider. Collectors describe the host, not a "
"framework: there is no cis_collector.py." % p)
if not offenders:
print(" OK %d production files: none named after a framework provider"
% len(tracked))

if FAIL:
print("=== native control catalog gate FAILED ===")
print(" The control is ISEDRAF's, or it is not a native control.")
sys.exit(1)
29 changes: 29 additions & 0 deletions scripts/ci/falsifiable.sh
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,35 @@ p.write_text(s.replace(old, "echo \"X-Build-Stamp: $(date +%s%N)\" >> \"$DEBROOT
PYX' \
'reproducible build gate FAILED|artifacts identical'

# D-111. The native control catalog is authored first and is ISEDRAF's own. The invariant
# is frozen; these prove the gate enforcing it can refuse.
inject "D-111 a production module is named after a framework provider" \
'python3 scripts/ci/check_native_catalog.py' \
'cp lib/isedraf/identity.py lib/isedraf/cis_collector.py && git add -f -A' \
'named after a framework provider|native control catalog gate FAILED'

inject "D-111 the namespace and the catalog document disagree" \
'python3 scripts/ci/check_native_catalog.py' \
'python3 - <<'"'"'PYX'"'"'
import json, pathlib
p = pathlib.Path("scripts/ci/native_controls.json"); d = json.loads(p.read_text())
d["families"]["ISE-GHOST"] = "a family the catalog document has never heard of"
p.write_text(json.dumps(d, indent=2))
PYX' \
'registry and not in the catalog document|native control catalog gate FAILED'

inject "D-111 a native criterion is derived from a framework" \
'python3 scripts/ci/check_native_catalog.py' \
'python3 - <<'"'"'PYX'"'"'
import json, pathlib
p = pathlib.Path("scripts/ci/native_controls.json"); d = json.loads(p.read_text())
d["criteria"].append({f: "x" for f in d["required_criterion_fields"]})
d["criteria"][0]["criterion_id"] = "ISE-SSH-001"
d["criteria"][0]["purpose"] = "Implements CIS Controls safeguard 4.1"
p.write_text(json.dumps(d, indent=2))
PYX' \
'names CIS in the criterion itself|native control catalog gate FAILED'

# D-84/D-90. Third-party framework content is not relicensed by sitting in this
# repository. The registry is deny-by-default, and "deny by default" is a claim that has
# to be shown to deny something.
Expand Down
9 changes: 9 additions & 0 deletions scripts/ci/gate_coverage.json
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,15 @@
"REUSE.toml",
"scripts/ci/framework_sources.json"
]
},
"check-native-catalog": {
"script": "scripts/ci/check_native_catalog.py",
"implements": "D-78, D-79, D-84, D-111",
"falsification": "D-111 a production module is named after a framework provider",
"eligible": [
"scripts/ci/native_controls.json",
"docs/architecture/NATIVE_CONTROL_CATALOG.md"
]
}
},
"unpublished_paths": {
Expand Down
Loading
Loading