From f08c8c6cc0e8d27f77b872e74b056aeba67c4c7c Mon Sep 17 00:00:00 2001 From: Antonios Voulvoulis Date: Tue, 29 Sep 2026 10:30:49 +0300 Subject: [PATCH 1/2] =?UTF-8?q?ISEDRAF=200.1.0=20=E2=80=94=20General=20Ava?= =?UTF-8?q?ilability=20(sanitized=20release=20export)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The public tree is replaced by the sanitized release export of the engineering repository at 2bc2d7f plus A-017 (D-110 publication model): the 0.1.0 product - the unprivileged `isedraf audit` path, the user-mode evidence store, reports rendered from committed evidence - the release metadata (GA date 2026-09-28), and the OpenSSF Baseline Level 2 work: SECURITY.md with response targets and coordinated disclosure, MAINTAINERS.md, a GA CONTRIBUTING.md with the DCO, docs/DEPENDENCIES.md, the DCO pull-request check, a repository-security gate, a release-export gate, Scorecard result publishing, and the GA push policy (git-hooks/pre-push: the engineering tree is never pushed, main never directly; it replaces the prototype's unconditional refusal). The export's own gates passed on it (privacy, documentation truth, freeze). No tag or release is created by this change; publication is the maintainer's. Assisted-by: Claude (implementation agent, Claude Code) Signed-off-by: Antonios Voulvoulis --- .github/ISSUE_TEMPLATE/bug.yml | 4 +- .github/ISSUE_TEMPLATE/collection_failure.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 2 +- .github/ISSUE_TEMPLATE/false_negative.yml | 4 +- .github/ISSUE_TEMPLATE/false_positive.yml | 2 +- .../ISSUE_TEMPLATE/platform_compatibility.yml | 2 +- .github/workflows/governance.yml | 19 + .github/workflows/scorecard.yml | 8 +- CHANGELOG.md | 44 +- CONTRIBUTING.md | 153 +- MAINTAINERS.md | 43 + Makefile | 144 +- README.md | 414 +-- REUSE.toml | 27 + SECURITY.md | 84 +- SUPPORT.md | 7 +- VERSION | 2 +- bin/isedraf | 5 +- docs/AUDITOR_GUIDE.md | 156 ++ docs/CURRENT_STATE.md | 9 +- docs/DEPENDENCIES.md | 52 + docs/EVIDENCE_MODEL.md | 136 + docs/GETTING_STARTED.md | 149 ++ docs/IMPLEMENTATION_QUESTIONS.md | 27 + docs/README.md | 5 + docs/REPORT_GUIDE.md | 134 + docs/REPOSITORY_MAP.md | 4 +- docs/SECURITY_AND_LIMITATIONS.md | 126 + docs/architecture/EVIDENCE_AND_TRUST_MODEL.md | 4 + docs/architecture/ISEDRAF_HLD.md | 15 +- docs/architecture/ISEDRAF_PRODUCT_HLD.md | 56 + docs/architecture/NORMATIVE_SOURCES.md | 1 + .../SNAPSHOT_BASELINE_DELTA_MODEL.md | 70 +- .../architecture/V0_1_IMPLEMENTATION_SCOPE.md | 9 +- .../freeze/W1A_CORE_PUBLIC.sha256 | 12 +- docs/compatibility/PYTHON_FLOOR_EVIDENCE.md | 4 +- docs/development/ACCOUNTS_HOSTIO_HARDENING.md | 169 ++ docs/development/ARCHITECTURE_ALIGNMENT.md | 299 +++ .../AUTHORIZED_KEYS_LANE_CONTRACT.md | 287 ++ docs/development/CI_INVENTORY.md | 2 +- docs/development/DOCUMENTATION_POLICY.md | 6 +- docs/development/DOC_R15P_RECONCILIATION.md | 125 + docs/development/GA_CLI_AUDIT.md | 69 + docs/development/GOVERNANCE_GAPS.md | 36 +- docs/development/HLD_ALIGNMENT_REVIEW_P2.md | 966 +++++++ docs/development/INVENTORY_COVERAGE_MATRIX.md | 431 +++ docs/development/LOGINPOLICY_LANE_CONTRACT.md | 50 + .../MOUNTS_DECLARED_VS_ACTIVE_CONTRACT.md | 436 ++++ .../development/NSS_HOSTNAME_LANE_CONTRACT.md | 559 ++++ docs/development/PAM_LANE_CONTRACT.md | 56 + docs/development/PUBLIC_DOCS_UX.md | 178 ++ ...P2_LEAST_AUTHORITY_ACQUISITION_CONTRACT.md | 1055 ++++++++ .../R15P_PRIVILEGE_AND_EVIDENCE_CONTRACT.md | 319 +++ docs/development/ROOTED_PATH_CONTRACT.md | 151 ++ .../SNAP_COVERAGE_BINDING_PROPOSAL.md | 163 ++ docs/development/SSH_LANE_CONTRACT.md | 58 + docs/development/SUDO_LANE_CONTRACT.md | 43 + .../TRUST_AND_ASSURANCE_DOCTRINE.md | 213 ++ docs/development/W1A_CERTIFICATION_MATRIX.md | 4 +- .../development/W1A_VECTOR_TRANSITION_D115.md | 70 + .../W1D_ACCOUNT_SOURCE_CONTRACT.md | 488 ++++ docs/development/architecture/ARCH-01.md | 109 + docs/development/architecture/ARCH-02.md | 132 + .../generated/01_module_dependencies.mmd | 209 ++ .../generated/02_module_dependencies.json | 456 ++++ .../generated/03_critical_call_trees.md | 78 + .../generated/04_evidence_flow.mmd | 14 + .../generated/05_status_semantics.md | 50 + .../generated/06_field_lineage.csv | 134 + .../generated/07_io_side_effects.csv | 138 + .../generated/08_trust_boundaries.mmd | 16 + .../generated/09_gate_ci_graph.mmd | 45 + .../generated/10_falsification_coverage.md | 28 + docs/operator/STORAGE_AND_OUTPUTS.md | 139 +- docs/reference/CONTROL_EVIDENCE_MAP.md | 6 +- docs/reference/samples/SAMPLE_REPORT.json | 16 +- docs/reference/samples/SAMPLE_REPORT.md | 17 +- docs/roadmap/ROADMAP.md | 220 +- git-hooks/pre-commit | 47 +- git-hooks/pre-push | 25 +- lib/isedraf/__init__.py | 4 +- lib/isedraf/accounts/acquire.py | 807 ++++++ lib/isedraf/accounts/model.py | 282 ++ lib/isedraf/accounts/sources.py | 668 +++++ lib/isedraf/audit.py | 106 + lib/isedraf/authorizedkeys/acquire.py | 587 +++++ lib/isedraf/authorizedkeys/model.py | 272 ++ lib/isedraf/authorizedkeys/sources.py | 354 +++ lib/isedraf/canonical.py | 3 + lib/isedraf/cli.py | 129 +- lib/isedraf/coverage.py | 321 +++ lib/isedraf/hostio.py | 215 ++ lib/isedraf/hostname/acquire.py | 153 ++ lib/isedraf/hostname/model.py | 74 + lib/isedraf/hostname/sources.py | 123 + lib/isedraf/hostpath.py | 137 + lib/isedraf/inventory/__init__.py | 57 + lib/isedraf/inventory/_exec.py | 128 - lib/isedraf/inventory/collectors.py | 189 +- lib/isedraf/inventory/model.py | 39 +- lib/isedraf/loginpolicy/acquire.py | 227 ++ lib/isedraf/loginpolicy/model.py | 118 + lib/isedraf/loginpolicy/sources.py | 71 + lib/isedraf/mounts/acquire.py | 283 ++ lib/isedraf/mounts/model.py | 243 ++ lib/isedraf/mounts/sources.py | 223 ++ lib/isedraf/nss/acquire.py | 217 ++ lib/isedraf/nss/model.py | 112 + lib/isedraf/nss/sources.py | 218 ++ lib/isedraf/pam/acquire.py | 187 ++ lib/isedraf/pam/model.py | 79 + lib/isedraf/pam/sources.py | 140 + lib/isedraf/report/__init__.py | 2 +- lib/isedraf/report/model.py | 118 +- lib/isedraf/report/render.py | 133 +- lib/isedraf/report/sections.py | 103 + lib/isedraf/runtime-imports.allow | 30 + lib/isedraf/shared/bounded.py | 276 ++ lib/isedraf/shared/compare.py | 473 ++++ lib/isedraf/shared/filemeta.py | 255 ++ lib/isedraf/shared/include_graph.py | 308 +++ lib/isedraf/shared/keyvalue.py | 331 +++ lib/isedraf/shared/result.py | 166 ++ lib/isedraf/snapshot.py | 95 +- lib/isedraf/ssh/acquire.py | 178 ++ lib/isedraf/ssh/model.py | 76 + lib/isedraf/ssh/sources.py | 173 ++ lib/isedraf/stateroot.py | 134 +- lib/isedraf/status.py | 42 + lib/isedraf/sudo/acquire.py | 217 ++ lib/isedraf/sudo/model.py | 102 + lib/isedraf/sudo/sources.py | 246 ++ lib/isedraf/textbytes.py | 61 + lib/isedraf/verify.py | 59 +- packaging/build.sh | 26 +- packaging/deb/control.in | 15 +- packaging/rpm/isedraf.spec.in | 37 +- scripts/analysis/_common.py | 92 + .../arch01_prose/03_critical_call_trees.md | 74 + .../arch01_prose/04_evidence_flow.mmd | 14 + .../arch01_prose/05_status_semantics.md | 46 + .../arch01_prose/08_trust_boundaries.mmd | 16 + scripts/analysis/artifacts.py | 190 ++ scripts/analysis/io_surface.py | 96 + scripts/analysis/module_graph.py | 135 + scripts/ci/check_architecture.py | 300 +++ scripts/ci/check_architecture_artifacts.py | 147 ++ scripts/ci/check_bootstrap_scope.sh | 10 +- scripts/ci/check_dco.py | 130 + scripts/ci/check_docs_truth.py | 21 + scripts/ci/check_lane_scope.sh | 70 + scripts/ci/check_package_payload.sh | 4 +- scripts/ci/check_packaging.py | 14 +- scripts/ci/check_paths.sh | 7 +- scripts/ci/check_precommit_index.sh | 113 + scripts/ci/check_public_claims.py | 11 +- scripts/ci/check_repo_security.py | 162 ++ scripts/ci/check_runtime_imports.py | 138 + scripts/ci/falsifiable.sh | 2184 ++++++++++++++-- scripts/ci/falsifiable_lib.sh | 73 +- scripts/ci/gate_coverage.json | 79 + scripts/ci/lane_manifests.json | 128 + scripts/ci/privacy_allowlist.json | 5 +- scripts/ci/project_status.json | 9 +- scripts/ci/public_layer.json | 32 + scripts/ci/public_licensing_policy.json | 4 +- scripts/ci/release_export.sh | 13 +- scripts/compat/package_lifecycle.sh | 146 +- scripts/docs/current_state.py | 7 +- scripts/docs/doclint.py | 9 +- scripts/docs/master_index.py | 19 +- scripts/docs/public_ux.py | 554 ++++ scripts/docs/sample_report.py | 6 + scripts/vectors/generate.py | 17 + scripts/vectors/verify.py | 35 +- .../expected/manifest-core.canonical | 2 +- .../expected/manifest-hash.txt | 2 +- .../expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../expected/record-hash.txt | 2 +- .../01-valid-machine-id/expected/record.json | 2 +- .../expected/manifest-core.canonical | 2 +- .../expected/manifest-hash.txt | 2 +- .../02-valid-with-lf/expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../02-valid-with-lf/expected/record-hash.txt | 2 +- .../v1/02-valid-with-lf/expected/record.json | 2 +- .../expected/manifest-core.canonical | 2 +- .../expected/manifest-hash.txt | 2 +- .../expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../expected/record-hash.txt | 2 +- .../expected/record.json | 2 +- .../expected/manifest-core.canonical | 2 +- .../expected/manifest-hash.txt | 2 +- .../04-missing-source/expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../expected/record-hash.txt | 2 +- .../v1/04-missing-source/expected/record.json | 2 +- .../expected/manifest-core.canonical | 2 +- .../expected/manifest-hash.txt | 2 +- .../05-invalid-length/expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../expected/record-hash.txt | 2 +- .../v1/05-invalid-length/expected/record.json | 2 +- .../expected/manifest-core.canonical | 2 +- .../06-invalid-hex/expected/manifest-hash.txt | 2 +- .../v1/06-invalid-hex/expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../06-invalid-hex/expected/record-hash.txt | 2 +- .../v1/06-invalid-hex/expected/record.json | 2 +- .../expected/manifest-core.canonical | 2 +- .../expected/manifest-hash.txt | 2 +- .../expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../expected/record-hash.txt | 2 +- .../07-extra-whitespace/expected/record.json | 2 +- .../expected/manifest-core.canonical | 2 +- .../expected/manifest-hash.txt | 2 +- .../expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../expected/record-hash.txt | 2 +- .../08-embedded-newline/expected/record.json | 2 +- .../expected/manifest-core.canonical | 2 +- .../09-double-lf/expected/manifest-hash.txt | 2 +- .../v1/09-double-lf/expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../v1/09-double-lf/expected/record-hash.txt | 2 +- .../w1a/v1/09-double-lf/expected/record.json | 2 +- .../expected/manifest-core.canonical | 2 +- .../v1/10-all-zero/expected/manifest-hash.txt | 2 +- .../w1a/v1/10-all-zero/expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../v1/10-all-zero/expected/record-hash.txt | 2 +- .../w1a/v1/10-all-zero/expected/record.json | 2 +- .../expected/manifest-core.canonical | 2 +- .../expected/manifest-hash.txt | 2 +- .../11-uninitialized/expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../11-uninitialized/expected/record-hash.txt | 2 +- .../v1/11-uninitialized/expected/record.json | 2 +- .../expected/manifest-core.canonical | 2 +- .../12-over-long/expected/manifest-hash.txt | 2 +- .../v1/12-over-long/expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../v1/12-over-long/expected/record-hash.txt | 2 +- .../w1a/v1/12-over-long/expected/record.json | 2 +- .../expected/manifest-core.canonical | 2 +- .../v1/13-non-utf8/expected/manifest-hash.txt | 2 +- .../w1a/v1/13-non-utf8/expected/manifest.json | 2 +- .../expected/record-core.canonical | 2 +- .../v1/13-non-utf8/expected/record-hash.txt | 2 +- .../w1a/v1/13-non-utf8/expected/record.json | 2 +- .../D1-domain-separation/expected/domains.txt | 1 + test-vectors/w1a/v1/EXPECTED.sha256 | 158 +- tests/fixtures/glibc_differential/corpus.json | 1 + tests/glibc_differential.py | 532 ++++ tests/glibc_oracle_driver.py | 180 ++ tests/test_accounts.py | 1734 ++++++++++++ tests/test_audit.py | 197 ++ tests/test_authorizedkeys.py | 773 ++++++ tests/test_authorizedkeys_adversarial.py | 1762 +++++++++++++ tests/test_auxiliary_binding.py | 258 ++ tests/test_cli.py | 69 + tests/test_coverage.py | 650 +++++ tests/test_glibc_differential.py | 66 + tests/test_hostio.py | 112 + tests/test_hostname.py | 268 ++ tests/test_hostpath.py | 264 ++ tests/test_identity.py | 36 +- tests/test_inventory.py | 87 + tests/test_launcher.py | 74 + tests/test_loginpolicy.py | 255 ++ tests/test_loginpolicy_adversarial.py | 280 ++ tests/test_mounts_adversarial.py | 2319 +++++++++++++++++ tests/test_nss.py | 588 +++++ tests/test_pam.py | 218 ++ tests/test_pam_adversarial.py | 263 ++ tests/test_prepush.py | 80 + tests/test_report.py | 20 +- tests/test_report_html.py | 107 + tests/test_shared_bounded.py | 301 +++ tests/test_shared_compare.py | 640 +++++ tests/test_shared_filemeta.py | 278 ++ tests/test_shared_include_graph.py | 470 ++++ tests/test_shared_keyvalue.py | 341 +++ tests/test_ssh.py | 242 ++ tests/test_ssh_adversarial.py | 403 +++ tests/test_stateroot.py | 176 ++ tests/test_sudo.py | 182 ++ tests/test_sudo_adversarial.py | 552 ++++ 291 files changed, 39424 insertions(+), 1248 deletions(-) create mode 100644 MAINTAINERS.md create mode 100644 docs/AUDITOR_GUIDE.md create mode 100644 docs/DEPENDENCIES.md create mode 100644 docs/EVIDENCE_MODEL.md create mode 100644 docs/GETTING_STARTED.md create mode 100644 docs/REPORT_GUIDE.md create mode 100644 docs/SECURITY_AND_LIMITATIONS.md create mode 100644 docs/development/ACCOUNTS_HOSTIO_HARDENING.md create mode 100644 docs/development/ARCHITECTURE_ALIGNMENT.md create mode 100644 docs/development/AUTHORIZED_KEYS_LANE_CONTRACT.md create mode 100644 docs/development/DOC_R15P_RECONCILIATION.md create mode 100644 docs/development/GA_CLI_AUDIT.md create mode 100644 docs/development/HLD_ALIGNMENT_REVIEW_P2.md create mode 100644 docs/development/INVENTORY_COVERAGE_MATRIX.md create mode 100644 docs/development/LOGINPOLICY_LANE_CONTRACT.md create mode 100644 docs/development/MOUNTS_DECLARED_VS_ACTIVE_CONTRACT.md create mode 100644 docs/development/NSS_HOSTNAME_LANE_CONTRACT.md create mode 100644 docs/development/PAM_LANE_CONTRACT.md create mode 100644 docs/development/PUBLIC_DOCS_UX.md create mode 100644 docs/development/R15P2_LEAST_AUTHORITY_ACQUISITION_CONTRACT.md create mode 100644 docs/development/R15P_PRIVILEGE_AND_EVIDENCE_CONTRACT.md create mode 100644 docs/development/ROOTED_PATH_CONTRACT.md create mode 100644 docs/development/SNAP_COVERAGE_BINDING_PROPOSAL.md create mode 100644 docs/development/SSH_LANE_CONTRACT.md create mode 100644 docs/development/SUDO_LANE_CONTRACT.md create mode 100644 docs/development/TRUST_AND_ASSURANCE_DOCTRINE.md create mode 100644 docs/development/W1A_VECTOR_TRANSITION_D115.md create mode 100644 docs/development/W1D_ACCOUNT_SOURCE_CONTRACT.md create mode 100644 docs/development/architecture/ARCH-01.md create mode 100644 docs/development/architecture/ARCH-02.md create mode 100644 docs/development/architecture/generated/01_module_dependencies.mmd create mode 100644 docs/development/architecture/generated/02_module_dependencies.json create mode 100644 docs/development/architecture/generated/03_critical_call_trees.md create mode 100644 docs/development/architecture/generated/04_evidence_flow.mmd create mode 100644 docs/development/architecture/generated/05_status_semantics.md create mode 100644 docs/development/architecture/generated/06_field_lineage.csv create mode 100644 docs/development/architecture/generated/07_io_side_effects.csv create mode 100644 docs/development/architecture/generated/08_trust_boundaries.mmd create mode 100644 docs/development/architecture/generated/09_gate_ci_graph.mmd create mode 100644 docs/development/architecture/generated/10_falsification_coverage.md create mode 100644 lib/isedraf/accounts/acquire.py create mode 100644 lib/isedraf/accounts/model.py create mode 100644 lib/isedraf/accounts/sources.py create mode 100644 lib/isedraf/audit.py create mode 100644 lib/isedraf/authorizedkeys/acquire.py create mode 100644 lib/isedraf/authorizedkeys/model.py create mode 100644 lib/isedraf/authorizedkeys/sources.py create mode 100644 lib/isedraf/coverage.py create mode 100644 lib/isedraf/hostio.py create mode 100644 lib/isedraf/hostname/acquire.py create mode 100644 lib/isedraf/hostname/model.py create mode 100644 lib/isedraf/hostname/sources.py create mode 100644 lib/isedraf/hostpath.py delete mode 100644 lib/isedraf/inventory/_exec.py create mode 100644 lib/isedraf/loginpolicy/acquire.py create mode 100644 lib/isedraf/loginpolicy/model.py create mode 100644 lib/isedraf/loginpolicy/sources.py create mode 100644 lib/isedraf/mounts/acquire.py create mode 100644 lib/isedraf/mounts/model.py create mode 100644 lib/isedraf/mounts/sources.py create mode 100644 lib/isedraf/nss/acquire.py create mode 100644 lib/isedraf/nss/model.py create mode 100644 lib/isedraf/nss/sources.py create mode 100644 lib/isedraf/pam/acquire.py create mode 100644 lib/isedraf/pam/model.py create mode 100644 lib/isedraf/pam/sources.py create mode 100644 lib/isedraf/report/sections.py create mode 100644 lib/isedraf/runtime-imports.allow create mode 100644 lib/isedraf/shared/bounded.py create mode 100644 lib/isedraf/shared/compare.py create mode 100644 lib/isedraf/shared/filemeta.py create mode 100644 lib/isedraf/shared/include_graph.py create mode 100644 lib/isedraf/shared/keyvalue.py create mode 100644 lib/isedraf/shared/result.py create mode 100644 lib/isedraf/ssh/acquire.py create mode 100644 lib/isedraf/ssh/model.py create mode 100644 lib/isedraf/ssh/sources.py create mode 100644 lib/isedraf/status.py create mode 100644 lib/isedraf/sudo/acquire.py create mode 100644 lib/isedraf/sudo/model.py create mode 100644 lib/isedraf/sudo/sources.py create mode 100644 lib/isedraf/textbytes.py create mode 100644 scripts/analysis/_common.py create mode 100644 scripts/analysis/arch01_prose/03_critical_call_trees.md create mode 100644 scripts/analysis/arch01_prose/04_evidence_flow.mmd create mode 100644 scripts/analysis/arch01_prose/05_status_semantics.md create mode 100644 scripts/analysis/arch01_prose/08_trust_boundaries.mmd create mode 100644 scripts/analysis/artifacts.py create mode 100644 scripts/analysis/io_surface.py create mode 100644 scripts/analysis/module_graph.py create mode 100644 scripts/ci/check_architecture.py create mode 100644 scripts/ci/check_architecture_artifacts.py create mode 100644 scripts/ci/check_dco.py create mode 100755 scripts/ci/check_lane_scope.sh create mode 100755 scripts/ci/check_precommit_index.sh create mode 100644 scripts/ci/check_repo_security.py create mode 100644 scripts/ci/check_runtime_imports.py create mode 100644 scripts/ci/lane_manifests.json create mode 100644 scripts/ci/public_layer.json create mode 100644 scripts/docs/public_ux.py create mode 100644 tests/fixtures/glibc_differential/corpus.json create mode 100644 tests/glibc_differential.py create mode 100644 tests/glibc_oracle_driver.py create mode 100644 tests/test_accounts.py create mode 100644 tests/test_audit.py create mode 100644 tests/test_authorizedkeys.py create mode 100644 tests/test_authorizedkeys_adversarial.py create mode 100644 tests/test_auxiliary_binding.py create mode 100644 tests/test_cli.py create mode 100644 tests/test_coverage.py create mode 100644 tests/test_glibc_differential.py create mode 100644 tests/test_hostio.py create mode 100644 tests/test_hostname.py create mode 100644 tests/test_hostpath.py create mode 100644 tests/test_launcher.py create mode 100644 tests/test_loginpolicy.py create mode 100644 tests/test_loginpolicy_adversarial.py create mode 100644 tests/test_mounts_adversarial.py create mode 100644 tests/test_nss.py create mode 100644 tests/test_pam.py create mode 100644 tests/test_pam_adversarial.py create mode 100644 tests/test_prepush.py create mode 100644 tests/test_report_html.py create mode 100644 tests/test_shared_bounded.py create mode 100644 tests/test_shared_compare.py create mode 100644 tests/test_shared_filemeta.py create mode 100644 tests/test_shared_include_graph.py create mode 100644 tests/test_shared_keyvalue.py create mode 100644 tests/test_ssh.py create mode 100644 tests/test_ssh_adversarial.py create mode 100644 tests/test_stateroot.py create mode 100644 tests/test_sudo.py create mode 100644 tests/test_sudo_adversarial.py diff --git a/.github/ISSUE_TEMPLATE/bug.yml b/.github/ISSUE_TEMPLATE/bug.yml index 59ac65d..af2d946 100644 --- a/.github/ISSUE_TEMPLATE/bug.yml +++ b/.github/ISSUE_TEMPLATE/bug.yml @@ -4,7 +4,7 @@ labels: ["bug", "needs-evidence"] body: - type: markdown attributes: - value: "**Redact first** (`--redact`). Vulnerabilities go to `SECURITY.md`, never here." + value: "**Redact first**, by hand: 0.1 has no automatic redaction. Vulnerabilities go to `SECURITY.md`, never here." - type: input id: version attributes: {label: ISEDRAF version} @@ -42,5 +42,5 @@ body: id: sanitized attributes: label: Sanitized evidence - description: "Redacted output (`--redact`). Never paste an unredacted identity or evidence bundle." + description: "Output, redacted by hand. Never paste an unredacted identity or evidence bundle." validations: {required: true} diff --git a/.github/ISSUE_TEMPLATE/collection_failure.yml b/.github/ISSUE_TEMPLATE/collection_failure.yml index 550e949..1d5c169 100644 --- a/.github/ISSUE_TEMPLATE/collection_failure.yml +++ b/.github/ISSUE_TEMPLATE/collection_failure.yml @@ -62,5 +62,5 @@ body: id: sanitized attributes: label: Sanitized evidence - description: "Redacted output (`--redact`). Never paste an unredacted identity or evidence bundle." + description: "Output, redacted by hand. Never paste an unredacted identity or evidence bundle." validations: {required: true} diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 9ed604b..63b22c5 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -5,7 +5,7 @@ contact_links: about: "Report privately to contact@itcms.gr. A false PASS, unsafe privileged execution, evidence corruption or incorrect delta logic is security-sensitive." - name: Support, supported platforms and how to redact url: https://github.com/itcmsgr/isedraf/blob/main/SUPPORT.md - about: "What is safe to share, how to use --redact, and how collection failure differs from product failure." + about: "What is safe to share, how to redact by hand, and how collection failure differs from product failure." - name: Is it in scope? Check the host evidence boundary url: https://github.com/itcmsgr/isedraf/blob/main/docs/roadmap/ROADMAP.md about: "Firewalls, AV/EDR, IDS/IPS, WAF, SIEM, cloud controls, CVE correlation and whole-filesystem FIM are deliberately out of scope." diff --git a/.github/ISSUE_TEMPLATE/false_negative.yml b/.github/ISSUE_TEMPLATE/false_negative.yml index f789bd6..f5886ff 100644 --- a/.github/ISSUE_TEMPLATE/false_negative.yml +++ b/.github/ISSUE_TEMPLATE/false_negative.yml @@ -9,7 +9,7 @@ body: reported as passing, that is a security-sensitive defect — consider reporting it privately via `SECURITY.md` instead. - **Redact first.** Use `--redact`. + **Redact first.** ISEDRAF 0.1 has no automatic redaction: remove names, host names and key material by hand. - type: input id: version attributes: {label: ISEDRAF version} @@ -57,5 +57,5 @@ body: id: sanitized attributes: label: Sanitized evidence - description: "Redacted output (`--redact`). Never paste an unredacted identity or evidence bundle." + description: "Output, redacted by hand. Never paste an unredacted identity or evidence bundle." validations: {required: true} diff --git a/.github/ISSUE_TEMPLATE/false_positive.yml b/.github/ISSUE_TEMPLATE/false_positive.yml index 2e8ce0a..fdd1256 100644 --- a/.github/ISSUE_TEMPLATE/false_positive.yml +++ b/.github/ISSUE_TEMPLATE/false_positive.yml @@ -5,7 +5,7 @@ body: - type: markdown attributes: value: | - **Redact first.** Use `--redact` and review what remains. Never paste an unredacted identity or + **Redact first**, by hand (0.1 has no automatic redaction), and review what remains. Never paste an unredacted identity or evidence bundle. See `SUPPORT.md`. - type: input id: version diff --git a/.github/ISSUE_TEMPLATE/platform_compatibility.yml b/.github/ISSUE_TEMPLATE/platform_compatibility.yml index a3a5dd8..78a13d2 100644 --- a/.github/ISSUE_TEMPLATE/platform_compatibility.yml +++ b/.github/ISSUE_TEMPLATE/platform_compatibility.yml @@ -52,5 +52,5 @@ body: id: sanitized attributes: label: Sanitized evidence - description: "Redacted output (`--redact`). Never paste an unredacted identity or evidence bundle." + description: "Output, redacted by hand. Never paste an unredacted identity or evidence bundle." validations: {required: true} diff --git a/.github/workflows/governance.yml b/.github/workflows/governance.yml index ee774dd..d01b7a7 100644 --- a/.github/workflows/governance.yml +++ b/.github/workflows/governance.yml @@ -91,3 +91,22 @@ jobs: - run: make check-vectors check-vectors-negative check-tests # Byte determinism WITHIN the lane, across every interpreter present on the runner. - run: bash scripts/vectors/crossversion.sh + + dco: + # D-91, OpenSSF LE-01.01: every commit a pull request adds carries its author's DCO + # sign-off. Pull requests only: history before the policy is in no pull request range. + name: DCO + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: read + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 + with: + fetch-depth: 0 + persist-credentials: false + - env: + DCO_BASE: ${{ github.event.pull_request.base.sha }} + DCO_HEAD: ${{ github.event.pull_request.head.sha }} + run: make check-dco-pr diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 2b9de8f..9a09f4f 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -10,9 +10,9 @@ # job would fail permanently. Until the public repository exists this has NEVER RUN, # and no score may be displayed. A score is a measurement, not a decoration. # -# `results.sarif` is uploaded to code scanning so findings are reviewable in place; -# it is NOT published to the public Scorecard API (`publish_results: false`), because -# publishing is an outward-facing act that is the owner's to authorise. +# `results.sarif` is uploaded to code scanning so findings are reviewable in place, and +# the results are published to the public Scorecard API (`publish_results: true`), which +# the owner authorised on 2026-09-29 so the README badge shows a real measurement. name: Scorecard on: @@ -43,7 +43,7 @@ jobs: with: results_file: results.sarif results_format: sarif - publish_results: false + publish_results: true - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: scorecard-results diff --git a/CHANGELOG.md b/CHANGELOG.md index bcf1827..1a29819 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,13 +8,51 @@ Status: IMPLEMENTED Implements: D-88, §40 All notable changes to ISEDRAF are recorded here. Format follows Keep a Changelog; versioning follows -Semantic Versioning once a release exists. `VERSION` is the single source of the current version, and -`make check` verifies that `VERSION`, this file and the release date agree. +Semantic Versioning once a release exists. `VERSION` is the single source of the current version. ## [Unreleased] +## [0.1.0] - 2026-09-28 + +The first release of ISEDRAF. + ### Added -- Nothing released. The project is in the planning phase; see `docs/CURRENT_STATE.md`. +- Unprivileged Linux host evidence collection with one command, `isedraf audit`, across + ten domains: host and platform inventory, local accounts and groups, NSS configuration, + hostname, sudo, SSH server configuration, PAM, login policy, mounts and SSH authorized + keys. +- A user-mode evidence store at `$XDG_STATE_HOME/isedraf` or `~/.local/state/isedraf`, + private to the user (mode 0700). A store on a network or unknown filesystem is refused + before anything is written. +- One committed evidence run per audit: canonical JSON, hash-bound, recorded in a + hash-chained ledger and verified after every commit. +- `isedraf report`: a report of the latest committed run - Markdown, JSON, or a static HTML + page - rendered from committed evidence only. +- DEB and RPM packages for the system Python (3.6 or later), with no compiled code and no + third-party runtime module. + +### Upgrading from a pre-release +- The package upgrades 0.1.0-alpha1 in place and keeps the evidence in your store. +- Running `sudo isedraf` is refused, as before, and no longer leaves Python bytecode in + the installed package directory. +- Pre-release cleanup: If you previously ran 0.1.0-alpha1 with sudo, Python may have left + unowned bytecode under `/usr/lib/isedraf`. After uninstalling ISEDRAF and confirming the + package is no longer installed, the remaining ISEDRAF bytecode/directory may be removed + manually. + +### Limitations +- This release does not run with elevated privilege. Facts that need root, such as + `/etc/shadow` and sudoers, are reported NOT_TESTED: not observed, never passed. +- PARTIAL means some evidence could not be collected. It does not mean the host is secure + or insecure. +- There are no pass/fail judgements and no comparison between runs. A run with no + observed problem does not show that a host is uncompromised. +- Evidence is protected by filesystem permissions. It is not protected against a local + administrator. + +### Comparability +- First release: there is no earlier normalized state to compare with, and no baseline to + rebind. +# Maintainers and roles + +Status: IMPLEMENTED +Implements: D-91, D-92, D-93, D-110 + +## People with access to sensitive resources + +| Person | GitHub | Access | +|---|---|---| +| Antonios Voulvoulis (ITCMS) | `@itcmsgr` | repository administration, release publication, security advisories, repository and security settings | + +Nobody else holds write, administration or release access to this repository, and there are no +deploy keys. `.github/CODEOWNERS` routes every review to the maintainer. + +## Roles and responsibilities + +**Project owner and maintainer.** Sets project direction and scope, reviews and merges changes, owns the +architecture decisions, and administers the repository and its security settings. + +**Release authority.** The maintainer alone creates tags and GitHub Releases. Release artifacts are +built by the repository's release workflow, which attaches build provenance and an SBOM to each one. + +**Security response.** The maintainer receives private vulnerability reports, triages them, prepares +fixes privately and publishes advisories, following [`SECURITY.md`](SECURITY.md). + +**Contributors.** Anyone may propose changes through a pull request under +[`CONTRIBUTING.md`](CONTRIBUTING.md). Every commit carries a Developer Certificate of Origin sign-off, +and a pull request is merged only after the required checks pass and the maintainer has reviewed it. + +## AI assistance + +AI tools are used as development assistants. They hold no maintainer role, no repository access of +their own, and no authorship, ownership or copyright in the project. Work they assisted with is +reviewed and committed by a person, and disclosed on each commit with an `Assisted-by:` trailer, as +described in [`AI_ASSISTED_DEVELOPMENT.md`](AI_ASSISTED_DEVELOPMENT.md). + +## Contact + +Security: see [`SECURITY.md`](SECURITY.md). Everything else: `contact@itcms.gr`. diff --git a/Makefile b/Makefile index 4b877e7..0dd4385 100644 --- a/Makefile +++ b/Makefile @@ -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-provider-alignment check-native-catalog check-licensing check-public-claims check-deb-ordering check-reproducible check-sbom check-tests check-python-floor check-packaging check-storage-vocabulary check-native-catalog check-licensing check-public-claims check-privacy check-docs-truth check-current-state check-sample-report 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-provider-alignment check-native-catalog check-licensing check-public-claims check-deb-ordering check-reproducible check-sbom check-tests check-python-floor check-architecture check-architecture-artifacts check-packaging check-storage-vocabulary check-native-catalog check-licensing check-public-claims check-privacy check-docs-truth check-current-state check-sample-report check-headers check-docs check-scope check-shell check-refs check-paths check-index check-freeze check-precommit check-imports check-vectors check-vectors-negative check-vectors-crossversion check-gate-coverage check-falsifiable check-public-ux help -check: check-scope check-headers check-python-floor check-packaging check-storage-vocabulary check-native-catalog check-licensing check-public-claims check-privacy check-docs-truth check-current-state check-sample-report 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-architecture check-architecture-artifacts check-packaging check-storage-vocabulary check-native-catalog check-licensing check-public-claims check-privacy check-docs-truth check-current-state check-sample-report check-shell check-refs check-paths check-index check-freeze check-precommit check-imports check-vectors check-vectors-negative check-vectors-crossversion check-tests check-docs check-public-ux check-repo-security check-dco check-export @echo "make check: all gates passed" ## check-scope D-96: no product implementation before architecture freeze @@ -26,6 +26,13 @@ check-shell: @echo "--- shell syntax (D-12) ---" @set -e; for f in $$(git ls-files '*.sh' 'git-hooks/*'); do bash -n "$$f" || exit 1; done @echo " OK shell syntax clean" + @# IQ-018: `producer | grep -q` lets grep exit first; under pipefail the producer's + @# SIGPIPE (141) fails the pipeline and the check is skipped. Measured on the bytecode + @# gate: 0.1% idle, 6% under load. Capture first, then match a variable. + @bad="$$(git ls-files '*.sh' 'git-hooks/*' | xargs grep -nE '^[^#]*[|][[:space:]]*grep[^|]*[[:space:]](-[A-Za-z]*q|--quiet)' || true)"; \ + if [ -n "$$bad" ]; then printf '%s\n' "$$bad" | sed 's/^/ /'; \ + echo " FAIL early-exit reader in a pipeline: grep -q after | can skip a check under pipefail (IQ-018)"; exit 1; fi + @echo " OK no early-exit reader in any shell pipeline (IQ-018)" ## check-refs D-105: every cited requirement/decision ID resolves; no amendment cited as authority check-refs: @@ -71,6 +78,14 @@ check-freeze: @echo "--- freeze manifests (D-68) ---" @bash scripts/ci/check_freeze.sh +## check-precommit IQ-029: the pre-commit hook validates the committed content, not the tree +check-precommit: + @bash scripts/ci/check_precommit_index.sh + +## check-imports D-84: every runtime import is on the explicit allowlist; fail closed +check-imports: + @python3 scripts/ci/check_runtime_imports.py + ## check-index D-89/Q-15: MASTER_INDEX counts are generated, never hand-maintained check-index: @echo "--- master index freshness (Q-15) ---" @@ -89,6 +104,15 @@ check-packaging: check-sample-report: @python3 scripts/docs/sample_report.py check +## check-architecture ARCH-01: the evidence pipeline has no reverse edges +check-architecture: + @python3 scripts/ci/check_architecture.py --self-test + @python3 scripts/ci/check_architecture.py + +## check-architecture-artifacts ARCH-01: the diagrams still describe the code +check-architecture-artifacts: + @python3 scripts/ci/check_architecture_artifacts.py + ## check-storage-vocabulary D-114: a retired inference does not return as vocabulary check-storage-vocabulary: @python3 scripts/ci/check_storage_vocabulary.py --self-test @@ -106,6 +130,27 @@ check-provider-alignment: check-licensing: @python3 scripts/ci/check_licensing.py +## check-repo-security OpenSSF Baseline L2: least-privilege workflows, Scorecard, policy documents +check-repo-security: + @echo "--- repository security (OpenSSF Baseline, D-90) ---" + @python3 scripts/ci/check_repo_security.py + +## check-dco D-91: the DCO checker tells signed from unsigned commits (self-test) +check-dco: + @echo "--- DCO sign-off checker (D-91) ---" + @python3 scripts/ci/check_dco.py --self-test + +## check-export D-110: the sanitized release export is publishable (gates pass on it); +## packages are built from it only by the release workflow, so --no-build here +check-export: + @echo "--- release export (D-110) ---" + @d="$$(mktemp -d)"; bash scripts/ci/release_export.sh --no-build "$$d/export"; rc=$$?; rm -rf "$$d"; exit $$rc + +## check-dco-pr CI, pull requests only: every added commit is signed off by its author. +## Reads DCO_BASE and DCO_HEAD from the environment; never part of `make check`. +check-dco-pr: + @python3 scripts/ci/check_dco.py + ## check-public-claims C-01/D-88/D-90: a badge is a claim, and a claim must be backed check-public-claims: @python3 scripts/ci/check_public_claims.py @@ -149,6 +194,93 @@ check-tests: @out=$$(python3 tests/test_identity.py 2>&1); rc=$$?; \ if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_pam.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_pam_adversarial.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_ssh.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_ssh_adversarial.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_loginpolicy.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_loginpolicy_adversarial.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_sudo.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_sudo_adversarial.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_shared_compare.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_shared_bounded.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_shared_filemeta.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_shared_keyvalue.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_shared_include_graph.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_hostpath.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_authorizedkeys.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_authorizedkeys_adversarial.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_coverage.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_auxiliary_binding.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_accounts.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_hostio.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_glibc_differential.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_nss.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_hostname.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_report_html.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_stateroot.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_audit.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_cli.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_launcher.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi + @out=$$(python3 tests/test_prepush.py 2>&1); rc=$$?; \ + if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ + else echo "$$out"; exit $$rc; fi @out=$$(python3 tests/test_inventory.py 2>&1); rc=$$?; \ if [ $$rc -eq 0 ]; then echo "$$out" | tail -3; \ else echo "$$out"; exit $$rc; fi @@ -161,5 +293,13 @@ check-docs: @echo "--- documentation lint (D-87, D-88, D-89) ---" @python3 scripts/docs/doclint.py +## check-public-ux DOC-PUBLIC-UX-001: public documentation is written for a human reader +## REPORT-ONLY until milestone DOC-PUBLIC-01 (scripts/ci/public_layer.json "enforce"). +## The self-test is never report-only: every rule must be shown to fire. +check-public-ux: + @echo "--- public documentation UX (DOC-PUBLIC-UX-001, D-87, D-88) ---" + @python3 scripts/docs/public_ux.py --self-test + @python3 scripts/docs/public_ux.py + help: @grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## / /' diff --git a/README.md b/README.md index f295ba2..11595f6 100644 --- a/README.md +++ b/README.md @@ -2,349 +2,149 @@ **Linux Host Assurance with Approved Baselines, State Delta & Verifiable Evidence** -> Measure once. Map everywhere. Fix only the delta. - [![License: MPL-2.0](https://img.shields.io/badge/license-MPL--2.0-blue)](LICENSE) -[![Version](https://img.shields.io/badge/version-0.1.0--alpha1-lightgrey)](VERSION) -[![Status](https://img.shields.io/badge/status-technical%20preview-orange)](docs/CURRENT_STATE.md) +[![Version](https://img.shields.io/badge/version-0.1.0-lightgrey)](VERSION) +[![Status](https://img.shields.io/badge/status-general%20availability-green)](docs/CURRENT_STATE.md) [![Platforms](https://img.shields.io/badge/platforms-11%20Linux%20distributions%20measured-informational)](docs/reference/PLATFORM_COMPATIBILITY.md) +[![Governance](https://github.com/itcmsgr/isedraf/actions/workflows/governance.yml/badge.svg)](https://github.com/itcmsgr/isedraf/actions/workflows/governance.yml) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/itcmsgr/isedraf/badge)](https://scorecard.dev/viewer/?uri=github.com/itcmsgr/isedraf) [![OpenSSF Baseline](https://www.bestpractices.dev/projects/15010/baseline)](https://www.bestpractices.dev/en/projects/15010/baseline-1) -**CI / Correctness** - -[![Governance](https://github.com/itcmsgr/isedraf/actions/workflows/governance.yml/badge.svg)](https://github.com/itcmsgr/isedraf/actions/workflows/governance.yml) +> **Current release: 0.1.0 — General Availability.** ISEDRAF 0.1.0 provides production unprivileged +> Linux host evidence collection. Privileged full-audit execution is not included in this release. -> **Status:** Public technical preview. ISEDRAF is a codename. The source is published and the -> supply-chain controls run against it; **no release has been published**, so there is nothing to -> download from a releases page yet. Build from source if you want to try it. +## What is ISEDRAF? -> **Every mark above is backed by a control that runs, or by a file in this repository.** Decorative trust -> badges are not used. Badges that cannot yet be earned are listed in -> [Security & supply-chain assurance](#security--supply-chain-assurance) as *planned*, not displayed. +ISEDRAF is a command you run on one Linux host when you want evidence about that host. +It reads what the host exposes locally, records where each fact came from and whether it +could be read at all, and commits the result as a verifiable evidence run on local disk. -ISEDRAF is an open-source Linux host assurance project designed to bridge day-to-day system administration with security audit and governance evidence. +ISEDRAF 0.1 is deliberately small and unprivileged: -It collects security-relevant state that the local operating system can actually prove, normalizes that state into stable machine-readable evidence, creates explicit approved baselines, and reports meaningful changes between runs. +- it runs as your own user, never as root; +- it uses the system Python 3 (3.6 or later) and its standard library, nothing else; +- it has no daemon, no database, no network connection and no remote control; +- it is installed from a DEB or RPM package; +- it is built not to change the host it inspects. -The same evidence is intended to serve three views: +Approved baselines and state comparison, named in the subtitle above, are the direction of +the project. They are not part of 0.1. -- the system administrator — what changed and what needs attention; -- the security auditor — what was observed, how it was collected, and what can or cannot be concluded; -- the organization — portable JSON/JSONL evidence that can later be ingested into existing governance, inventory or analytics workflows. +## What does it collect? -## Core model +One `isedraf audit` reads ten areas of the host, once: -```text -local host evidence - ↓ -normalization - ↓ -immutable snapshot - ↓ -approved baseline - ↓ -comparable state - ↓ -classified delta - ↓ -explanation - ↓ -portable evidence +| Area | What is read | +|---|---| +| Host and platform | operating system, kernel, architecture, hardware and network inventory | +| Local accounts and groups | `/etc/passwd`, `/etc/group`, and `/etc/shadow` when readable | +| Name service (NSS) | which sources the system uses to look up accounts | +| Hostname | the declared hostname and the one the kernel is using | +| sudo | the sudo policy files, when readable | +| SSH server | the SSH daemon configuration files | +| PAM | the PAM configuration | +| Login policy | password ageing and login defaults | +| Mounts | declared and active mounts | +| SSH authorized keys | key files named by the SSH configuration | + +Each area gets a status: **COLLECTED**, **PARTIAL**, **NOT_TESTED** or **ERROR**. +Because 0.1 runs without root, anything that needs root (for example `/etc/shadow` or the +sudo policy) is reported as NOT_TESTED, with the reason. + +## What it does not claim + +- **NOT_TESTED is not a pass.** It means the fact was not observed. +- **It gives no verdicts.** There is no PASS/FAIL, no score and no findings in 0.1. +- **It is not a compliance tool.** Host evidence is not organizational compliance, and no + framework mapping exists in this repository. +- **It does not prove a host is uncompromised.** A local root user can alter what ISEDRAF + reads, and ISEDRAF itself. +- **It is not remote attestation.** Evidence is protected by file permissions on the host. +- **It does not assess firewalls, antivirus, EDR, SIEM, backups, cloud or network + controls, patch availability or CVEs.** A product missing from the host does not mean + the control is missing. + +The full list, with reasons, is in [Security and limitations](docs/SECURITY_AND_LIMITATIONS.md). + +## Install + +No published packages exist yet. Build them from a clean checkout of this repository: + +```sh +bash packaging/build.sh ``` -A baseline means accepted state, not automatically secure state. - -No observed security-relevant delta does not prove that a host is uncompromised. - -## Prototype scope - -The initial prototype is deliberately narrow: - -- host identity; -- local users and groups; -- local sudo privilege; -- `authorized_keys` fingerprints; -- password/account ageing; -- SSH resolved state; -- mounts — declared, resolved and active; -- audit subsystem state; -- journald recording coverage; -- time synchronization quality required for trustworthy recording evidence. - -The objective is not maximum control count. +The packages land in `dist/packages/`. Install the one for your distribution with the +system package manager, for example `apt install ./dist/packages/isedraf-latest_all.deb` +or `dnf install ./dist/packages/isedraf-latest.noarch.rpm`. +The package installs `/usr/bin/isedraf` and `/usr/lib/isedraf/`, and depends only on +`python3`. It creates no user and starts no service. -The first objective is trustworthy state and trustworthy delta. +[Getting started](docs/GETTING_STARTED.md) covers building, installing, and running from +a checkout without installing. -## Explicitly outside the host evidence boundary +## First run -ISEDRAF core does not assess: +Run it as your normal user. Do not use `sudo`: this release refuses to run as root. -- firewall effectiveness; -- AV/EDR/XDR; -- IDS/IPS; -- SIEM effectiveness; -- cloud controls; -- external network controls; -- WAF; -- external backup systems; -- remote patch availability; -- vulnerability-feed/CVE correlation. - -Absence of a locally detectable external product is not interpreted as absence of that security control. - -## Design principles - -ISEDRAF is designed around: - -- read-only host assessment; -- no privileged daemon; -- no internal sudo; -- no embedded database; -- no API server; -- no network egress from the core collector; -- canonical JSON/JSONL artifacts; -- immutable snapshots; -- explicit baseline approval; -- granular accepted changes; -- collection truth separate from evaluation; -- `NOT_TESTED` and `NOT_COMPARABLE` instead of invented PASS/FAIL; -- declared / resolved / active state where applicable; -- operator and auditor views derived from the same evidence. - -## v0.1.0-alpha1 — what is claimed, and what is not - -**ISEDRAF Technical Preview — Linux Host Assurance & Evidence Engine.** +```sh +isedraf audit # collect once and commit one evidence run +isedraf report --html --save # render that run as a static HTML file +``` -Implemented and observed: +`isedraf audit` prints one status line per area and exits: -| | | +| Exit | Meaning | |---|---| -| deterministic host identity | immutable identity evidence | -| hash-chained ledger | independent verification | -| host inventory | JSON + Markdown reports | -| DEB / RPM / source packages | Python 3.6+ production-code compatibility | -| validated Linux distribution families | CodeQL | -| Scorecard execution | SPDX SBOM | -| artifact attestations | tamper-verification | -| falsifiable internal gates | | - -**Not yet claimed** — each of these is absent on purpose, and none is coming in this release: +| 0 | the run was committed and every area was COLLECTED | +| 2 | the run was committed, but some areas are PARTIAL, NOT_TESTED or ERROR | +| 64 | usage or engine error, or the evidence store was refused | +| 70 | run as root or through sudo; refused, nothing was collected | -`GA / production readiness` · `all Linux distributions` · `ARM64 certification` · -`organizational compliance` · `CIS mapping` · `ISO 27001 mapping` · `NIS2 / DORA compliance` · -`PDF reports` · `privileged production Mode A` +Exit 2 is the normal result of an unprivileged run on a real host. -A technical preview is a thing you can install, inspect and verify. It is not a thing to run a -compliance programme on. +## What output do I get? -## Framework mappings +- **Evidence** in `~/.local/state/isedraf` (or `$XDG_STATE_HOME/isedraf`): one directory + per run, a hash-chained ledger, mode 0700, owned by you. +- **A report** rendered from the last committed run, as Markdown (default), JSON + (`--json`) or static HTML (`--html`). `--save` writes it into the evidence store. + A report never collects anything. -**None exist, none are bundled, and none are licensed.** No third-party control text, identifier set -or mapping dataset is present in this repository, in the packages or in the SBOM. +The HTML report opens with the privilege level and the overall evidence status, then one +section per area: its status, the reason when it is not COLLECTED, the facts observed, +and a reference to the evidence file and its digest. -The evidence model is designed to support optional, independently versioned framework mappings where -licensing and scope permit. That is a statement about architecture, not about availability. +Removing the package never deletes the evidence. -ISEDRAF's collectors and criteria are framework-neutral: they describe the Linux host, they are -authored by this project, and removing every framework would leave them exactly as useful. A mapping, -when one exists, is a downstream overlay on evidence that was already collected — never a reason a -collector was written. +## Where to read next -Licensing is decided before content arrives. `scripts/ci/framework_sources.json` is **deny by -default**, `make check-licensing` enforces it, and five defect injections prove it can refuse. See -[Framework mapping policy](docs/licensing/FRAMEWORK_MAPPING_POLICY.md). - -A mapping is not a certification, and host evidence is supporting technical evidence rather than -organisational compliance. - -## Scope - -ISEDRAF's scope is its own host-state, evidence, baseline and delta model. It does not implement -SCAP content, file-integrity monitoring, vulnerability scanning, telemetry query or log shipping, -and it makes no assessment of tools that do. - -The previous wording here named eight other projects in order to say ISEDRAF was not competing with -them. Naming them was itself the comparison: it placed ISEDRAF on the same axis and invited the -reader to make it. `C-06` forbids that, and `make check-docs-truth` now enforces it. - -## Security & supply-chain assurance - -What is true today, and verifiable from this repository: - -| Control | Where it is proven | -|---|---| -| Governance, header identity, shell syntax and documentation gates run on every push and pull request | `.github/workflows/governance.yml`, `make check` | -| Every gate is proven able to fail, by deliberate defect injection | `make check-falsifiable` | -| Every third-party GitHub Action is pinned to a full commit SHA, enforced rather than asserted | `.github/workflows/`, `make check-docs-truth` | -| Workflow tokens default to read-only; Actions cannot approve pull requests | repository Actions settings | -| The runtime imports only the Python standard library | `lib/isedraf/` source; a mechanical import-allowlist gate is PLANNED | -| No networking module is imported and no egress path exists in the collector | source inspection; a mechanical allowlist gate and any kernel-level restriction are PLANNED | -| Canonical artifacts are hashed with a named algorithm, and an independent verifier re-derives every hash from the stored preimages | `scripts/vectors/verify.py`, `test-vectors/w1a/v1/` | -| Security-sensitive failures are tested with deliberate negative cases | corpus acceptance tests | -| No real operator identifier reaches the publication surface | `make check-privacy` | -| Documentation references, action pins, competitive framing and quoted digests are checked mechanically | `make check-docs-truth` | -| CodeQL analyses both the Python **and** the GitHub Actions workflows, with the security-extended query suite | [`codeql.yml`](.github/workflows/codeql.yml) | -| OpenSSF Scorecard runs against this repository; results go to code scanning, and **no score is published or displayed** | [`scorecard.yml`](.github/workflows/scorecard.yml) | -| Release artifacts carry build provenance and an SBOM attestation, and the attestation has been **observed to refuse a forgery** — each artifact verifies, a copy with one flipped byte does not | [`check_attestation_falsifiable.sh`](scripts/ci/check_attestation_falsifiable.sh) | -| Packaging metadata is checked as text, on any machine, before a commit — a package that builds on the author's distribution is not a package | `make check-packaging` | -| The source tarball and the `.deb` rebuild **bit-for-bit on a different distribution**, and the locally rebuilt files verify against the attestation GitHub produced. The `.rpm` is **not** claimed byte-identical across rpm toolchain versions — rpm 4 and rpm 6 choose different payload compression, which is toolchain variation and not a different ISEDRAF payload | `make check-reproducible`, `make check-deb-ordering`, [`KGG-016`](docs/development/GOVERNANCE_GAPS.md) | -| A machine-readable SBOM describes each artifact, generated from the **final package** and checked against it — for the RPM, against `rpm`'s own recorded per-file digests | `scripts/ci/generate_sbom.py`, `make check-sbom` | -| Every tracked file carries a licence statement, and third-party framework content is deny-by-default: unknown licensing state means not distributable | `make check-licensing`, [`FRAMEWORK_SOURCE_REGISTRY`](docs/licensing/FRAMEWORK_SOURCE_REGISTRY.md) | -| Controls that are intended but **not** in force are written down, not glossed over | [`docs/development/GOVERNANCE_GAPS.md`](docs/development/GOVERNANCE_GAPS.md) | - -### Planned, not yet displayed - -These are deliberately absent until they are earned. - -Secret scanning remains unavailable on this plan — verified, not assumed — and a deterministic local -secret-pattern gate stands in for it, which is **not** equivalent: no partner-token feed, no historical -scan, no push-time enforcement. That substitution is written down in -[`KGG-002`](docs/development/GOVERNANCE_GAPS.md) rather than glossed over. - -`SLSA Build L3` — **not claimed**, and deliberately not claimed even though provenance now -exists: only once release artifacts genuinely meet the build-platform and provenance -requirements; the SLSA generator's own documentation states that using its workflows alone does not -satisfy every L3 obligation · `OpenSSF Best Practices` (earned by satisfying the criteria, not by inserting the image) · -`OSV-Scanner` · `Gitleaks` · `REUSE compliance` · -`SHA-256 release checksums` · `signed release artifacts` · -**`Baseline & delta invariants`** — the ISEDRAF-specific one: ten unchanged runs produce zero changes, -`NOT_TESTED` never becomes `REMOVED`, an engine upgrade produces zero false security changes, snapshots -stay immutable, and a new privileged user is detected. - -### Written, and never run - -A third state, between *in force* and *planned*, which this project needs a word for because -collapsing it into either one would be a claim the evidence has not earned: - -Nothing currently sits in this state: CodeQL, Scorecard, build provenance, SBOM attestation and -the control that proves an attestation refuses a forgery all ran for the first time on 2026-09-19 -and are listed in the table above instead. `KGG-011` records what they were before that, and is -closed. - -The mechanism stays, because it is what made the distinction honest while it lasted: each of those -jobs is conditional on the repository being public, a skipped job reports **green**, and so every -one of them is paired with a `guard` job that **fails** if the analysis was due and did not run. -`docs/CURRENT_STATE.md` still understands `WRITTEN_NEVER_RUN` as a status, and will use it again. - -### What may be said about reproducibility - -Three artifacts, three different strengths of claim, and they are not interchangeable: - -| Artifact | Claim | +| If you want to | Read | |---|---| -| source tarball | **cross-builder byte reproducibility demonstrated** | -| `.deb` | **cross-builder byte reproducibility demonstrated.** It also rebuilt byte-identically after a source-tree documentation-only change that did not alter its package payload | -| `.rpm` | **payload and package semantics consistent.** Byte reproducibility across rpm 4 / rpm 6 is **not claimed** | +| install, run, and fix a refused run | [Getting started](docs/GETTING_STARTED.md) | +| understand statuses, runs and verification | [Evidence model](docs/EVIDENCE_MODEL.md) | +| review a run as an auditor, and check a fact by hand | [Auditor guide](docs/AUDITOR_GUIDE.md) | +| read the report section by section | [Report guide](docs/REPORT_GUIDE.md) | +| know exactly what ISEDRAF cannot tell you | [Security and limitations](docs/SECURITY_AND_LIMITATIONS.md) | -Tested builders: Fedora 44 / btrfs / rpm 6.0.2 and `ubuntu-latest` / ext4 / rpm 4.18.2. `BUILDTIME` -was identical on both, so `SOURCE_DATE_EPOCH` taken from the commit works across toolchains; the -`.rpm` bytes differ because rpm 6 writes a **zstd** payload where rpm 4 writes **gzip**. - -**No `Reproducible Builds ✓` badge will be shown**, now or later, without a qualifier naming which -formats actually have byte-identical proof. A green tick beside three artifacts when two of them -qualify is the kind of claim this project exists not to make. - -### The rule - -Every green mark is clickable and leads to the evidence behind it — a workflow run, a release provenance -record with verification instructions, a live scorecard, or the criteria page. A badge that cannot link to -evidence is not added. - -Never used: *secure* · *audited* · *compliant* · *enterprise ready* · *100% tests* · *tamper proof*. -Those are marketing claims, not evidence. - -## What exists today - -ISEDRAF is an **early-stage prototype**. What follows is measured, not projected: - -| | | -|---|---| -| `isedraf identity` | `/etc/machine-id` → normalized → `host_id` → immutable snapshot → hash-chained ledger → independent verification | -| `isedraf inventory` | platform, machine, CPU, memory, storage, network with classified IPv6, DNS, time — every field classified as fact or observation | -| `isedraf report` | one report model, rendered as JSON and Markdown, with an optional assessment profile | +The reader test for these pages: a new administrator or auditor should be able to say what +ISEDRAF is, what it collected and did not collect, what a report tells them and does not +prove, where a fact came from, and what to read next, without the internal engineering +documents. If they cannot, that is a documentation defect; please report it. -Measured across **ten Linux distributions** — Debian 11/12/13, Ubuntu 22.04/24.04/26.04, AlmaLinux 8/9, -Rocky 9, CentOS Stream 9, openSUSE Leap 15.6 — producing **identical canonical bytes on CPython 3.6.8 -through 3.14.4**, with **no distribution-specific code**. All of it on `x86_64`; ARM64 is a first-class -target that **has not been tested yet**, and [the compatibility record](docs/reference/PLATFORM_COMPATIBILITY.md) -says so rather than implying otherwise. - -Nothing is released. See [the roadmap](docs/roadmap/ROADMAP.md). - -## Where ISEDRAF stores data - -```text -/etc/isedraf/ administrator configuration (PLANNED) -/var/lib/isedraf/ persistent canonical state and evidence -journal operational records, via journald (PLANNED) -/run/isedraf/ ephemeral runtime state (PLANNED) -``` - -**Logs explain the run. Evidence describes the host.** Back up `/var/lib/isedraf/`; log retention is a -separate concern and is not a substitute for evidence. - -Snapshots, the hash-chained ledger, reports, exports, permissions, identifiers and the development state -root are all covered in **[Storage and Outputs](docs/operator/STORAGE_AND_OUTPUTS.md)**, which is the -canonical reference and says which paths exist today. - -For how an observed fact maps to its Linux source, its normalized field, its evidence artifact and — crucially — what it does **not** prove, see -**[Control & Evidence Map](docs/reference/CONTROL_EVIDENCE_MAP.md)**. - -## Documentation - -Canonical technical documentation lives under: - -```text -docs/ -``` - -Architecture and implementation are traceable through frozen requirement IDs and the project decisions register. - -The GitHub Wiki is not authoritative. - -## Development status - -Implementation status is tracked in: - -```text -docs/CURRENT_STATE.md -``` - -Future capabilities described in architecture or roadmap material must not be interpreted as released functionality. +Engineering and design documents live under [`docs/`](docs/README.md); implementation +status is in [Current state](docs/CURRENT_STATE.md). ## Security -Do not publish sensitive host evidence or suspected vulnerabilities in ordinary issues. - -See: - -```text -SECURITY.md -``` - -Security contact: - -```text -contact@itcms.gr -``` +Do not put host evidence or suspected vulnerabilities in public issues. See +[SECURITY.md](SECURITY.md). Contact: contact@itcms.gr. ## AI-assisted development -AI systems may assist with design review, implementation, testing and documentation. - -They do not own the project or hold architectural authority. - -Final project decisions, acceptance, release authority and responsibility remain with Antonios Voulvoulis / ITCMS. - -See: - -```text -AI_ASSISTED_DEVELOPMENT.md -``` +ISEDRAF is developed with AI assistance. AI systems help with design review, +implementation, testing and documentation; they hold no ownership or architectural +authority. Decisions, acceptance, releases and responsibility remain with Antonios +Voulvoulis / ITCMS. See [AI_ASSISTED_DEVELOPMENT.md](AI_ASSISTED_DEVELOPMENT.md). ## License -ISEDRAF is licensed under the Mozilla Public License 2.0. - -Copyright © 2026 Antonios Voulvoulis / ITCMS. - -See `LICENSE`. +Mozilla Public License 2.0. Copyright © 2026 Antonios Voulvoulis / ITCMS. See [LICENSE](LICENSE). diff --git a/REUSE.toml b/REUSE.toml index 2d71166..16f03ee 100644 --- a/REUSE.toml +++ b/REUSE.toml @@ -41,6 +41,14 @@ precedence = "aggregate" SPDX-FileCopyrightText = "2026 Antonios Voulvoulis / ITCMS " SPDX-License-Identifier = "MPL-2.0" +# The recorded glibc differential corpus: generated synthetic inputs and glibc's answers to +# them. JSON cannot carry an inline licence header (L-07 forbids a synthetic licence key). +[[annotations]] +path = ["tests/fixtures/glibc_differential/**"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2026 Antonios Voulvoulis / ITCMS " +SPDX-License-Identifier = "MPL-2.0" + # Measured compatibility records and rendered report samples: generated evidence, strict # JSON, no comment syntax. [[annotations]] @@ -74,3 +82,22 @@ path = ["packaging/deb/control.in", "packaging/rpm/isedraf.spec.in", "packaging/ precedence = "aggregate" SPDX-FileCopyrightText = "2026 Antonios Voulvoulis / ITCMS " SPDX-License-Identifier = "MPL-2.0" + +# ARCH-01 generated structural artifacts: Mermaid, JSON and CSV emitted by +# scripts/analysis/. They are regenerated from the code rather than hand-maintained, so an +# inline header would be rewritten on every run - and CSV and JSON have no comment syntax +# that their consumers would tolerate. +[[annotations]] +path = ["docs/development/architecture/generated/*.mmd", "docs/development/architecture/generated/*.json", "docs/development/architecture/generated/*.csv"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2026 Antonios Voulvoulis / ITCMS " +SPDX-License-Identifier = "MPL-2.0" + +# ARCH-01 prose sources: the authored bodies that scripts/analysis/artifacts.py emits into +# the generated directory. An inline SPDX header here would be duplicated into the output, +# which already receives one from the generator. +[[annotations]] +path = ["scripts/analysis/arch01_prose/*"] +precedence = "aggregate" +SPDX-FileCopyrightText = "2026 Antonios Voulvoulis / ITCMS " +SPDX-License-Identifier = "MPL-2.0" diff --git a/SECURITY.md b/SECURITY.md index 2e704a6..c7e0c0a 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -7,16 +7,53 @@ SPDX-FileCopyrightText: Copyright (c) 2026 Antonios Voulvoulis / ITCMS Status: IMPLEMENTED Implements: D-30, D-31, D-35, D-46, §36 -## Reporting +## Supported versions -Report vulnerabilities privately to **`contact@itcms.gr`**. +| Version | Security fixes | +|---|---| +| 0.1.x (current release) | yes | +| pre-releases (0.1.0-alpha1 and release candidates) | no - upgrade to 0.1.x | + +## Reporting a vulnerability + +Report vulnerabilities **privately**, by either route: + +- **GitHub private vulnerability reporting** (preferred): the repository's *Security* tab → + *Report a vulnerability*. This opens a private security advisory visible only to you and the + maintainer. +- **Email:** `contact@itcms.gr`. -**Do not open a public issue for a vulnerability**, and do not include a working exploit or a step-by-step -extraction path in any public channel. A dedicated ISEDRAF security contact will be established before -public release; until then this address is authoritative. +**Do not open a public issue, discussion or pull request for a vulnerability**, and do not publish a +working exploit or a step-by-step extraction path while the issue is unresolved. -Include: affected version, platform and distribution, privilege level of the run, what you observed, what -you expected, and a minimal reproduction. **Redact before sending** — see `SUPPORT.md` and use `--redact`. +Include: affected version, platform and distribution, how ISEDRAF was run, what you observed, what you +expected, and a minimal reproduction. **Redact before sending:** ISEDRAF 0.1 has no automatic +redaction, so remove host names, account names, key fingerprints and sudo rule bodies by hand. See +`SUPPORT.md`. + +## Response targets + +These are targets the maintainer works to, not guarantees: + +| Step | Target | +|---|---| +| Acknowledgement of your report | within **3 business days** | +| Initial assessment and severity triage | within **10 business days** | +| Coordinated public disclosure | normally within **90 days** of the report | + +The disclosure date may be brought forward or extended by mutual agreement with the reporter where +there is a technical reason, for example a fix that needs more time or a vulnerability already being +exploited. Please do not disclose an unresolved vulnerability publicly before the coordinated date. + +## How vulnerabilities are published + +A confirmed vulnerability is fixed in a release and published through: + +- a **GitHub Security Advisory** on this repository; +- a **CVE** record where the issue warrants one (not every report receives a CVE); +- the release notes and `CHANGELOG.md`. + +Reporters are credited in the advisory unless they prefer otherwise. ## What counts as a security-sensitive defect @@ -25,34 +62,25 @@ no memory is corrupted and no privilege is gained. | Class | Why it is security-sensitive | |---|---| -| **False `PASS` caused by collection failure** | The operator believes a control was verified when it was never observed. Treated as **particularly serious**. | -| `NOT_TESTED` rendered as an improvement or a removal | Missing evidence silently becomes good news. | -| Incorrect baseline comparison | A real change is hidden, or a non-change is reported as drift. | -| Incorrect delta or classification logic | A security regression is classified as informational. | -| Snapshot or evidence corruption | The evidence record is no longer trustworthy. | -| Integrity-verifier failure | Tool integrity reporting cannot be relied upon. | -| Unsafe privileged execution | Anything that runs with more privilege than the frozen model allows. | +| **Evidence reported as observed when it was not collected** | The operator believes something was checked that never was. Treated as **particularly serious**. | +| `NOT_TESTED` presented as a good result | Missing evidence silently becomes good news. | +| Snapshot or evidence corruption not detected | The evidence record is no longer trustworthy. | +| Integrity-verifier failure | Verification of committed evidence cannot be relied upon. | +| Unsafe privileged execution | Anything that runs with more privilege than documented, or changes the host. | | Privilege escalation | Local escalation through ISEDRAF or its installed files. | -| Unsafe guidance | Illustrative remediation that would lock out or disrupt a host. | | Sensitive report disclosure | Identity data, key fingerprints, sudo rule bodies or GECOS leaking into a shared artifact. | -| Schema confusion | A consumer misreads exported evidence because versions or types are ambiguous. | -| Supply-chain issues | Package, update or build-provenance problems. | +| Supply-chain issues | Package, build or provenance problems. | ## What is not a vulnerability - ISEDRAF not detecting a change it never claimed to collect — check the evidence boundary first. -- The absence of a locally detectable external agent being reported as `NOT_TESTED` rather than a failure. - This is intended behaviour (D-09). -- A local signature not proving that root did not manipulate source data. ISEDRAF does not claim this. +- A fact that needs elevated privilege being reported as `NOT_TESTED`. ISEDRAF 0.1 runs unprivileged by + design. +- Local verification not defeating an administrator who alters both the evidence and its records. + ISEDRAF does not claim this. ## Scope and honest limits ISEDRAF observes userspace and kernel-exposed state on the local host. It is not remote attestation, and -local verification detects drift rather than defeating a root-level adversary on the same host. These -limits are documented, not defects. - -## Handling - -Acknowledgement is sent on receipt. Reports are triaged by severity, with false `PASS` and unsafe -privileged execution treated as highest. Fixes are developed privately and released with a -`CHANGELOG.md` entry. Reporters are credited unless they prefer otherwise. +local verification detects change rather than defeating a root-level adversary on the same host. These +limits are documented in `docs/SECURITY_AND_LIMITATIONS.md`; they are not defects. diff --git a/SUPPORT.md b/SUPPORT.md index 8de179d..5547f4b 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -14,8 +14,9 @@ home paths, SSH key fingerprints, sudo rule bodies and GECOS fields. **Never post an unredacted identity or evidence bundle in a public issue, discussion or chat.** -Use `--redact`, which removes key fingerprints, sudo rule bodies and GECOS. Review what remains before -sharing. When in doubt, send it privately to `contact@itcms.gr` instead. +ISEDRAF 0.1 has no automatic redaction. Remove usernames, host names, key fingerprints, sudo rule +bodies and GECOS by hand, and review what remains before sharing. When in doubt, send it privately to +`contact@itcms.gr` instead. ## Safe to share @@ -43,7 +44,7 @@ expected to surface as `NOT_TESTED`, not as a passing result. It is a **product bug** if: ISEDRAF reports a passing result for something it could not collect · a real change is not detected in a supported domain on a supported platform · an unchanged host produces -drift · ISEDRAF writes outside `/var/lib/isedraf` · it modifies host state. +drift · ISEDRAF writes outside its evidence store · it modifies host state. It is an **unsupported environment** if: the distribution is outside the supported tier · a required subsystem tool is absent and the result is honestly reported as `NOT_TESTED` · the host uses an identity diff --git a/VERSION b/VERSION index a1ce7a1..6e8bf73 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.1.0-alpha1 +0.1.0 diff --git a/bin/isedraf b/bin/isedraf index dc72825..aea68dc 100755 --- a/bin/isedraf +++ b/bin/isedraf @@ -80,4 +80,7 @@ for lib in "$PREFIX/lib" "$PREFIX/lib/isedraf/.."; do fi done -exec "$PY" -c 'import sys; from isedraf.cli import main; sys.exit(main())' "$@" +# -B: never write bytecode (D-86). Under root the interpreter could write into the +# installed package, and a refused `sudo isedraf` left root-owned __pycache__ files in +# /usr/lib/isedraf that no package owns, so uninstall left the directory behind. +exec "$PY" -B -c 'import sys; from isedraf.cli import main; sys.exit(main())' "$@" diff --git a/docs/AUDITOR_GUIDE.md b/docs/AUDITOR_GUIDE.md new file mode 100644 index 0000000..d1699e7 --- /dev/null +++ b/docs/AUDITOR_GUIDE.md @@ -0,0 +1,156 @@ + +# Auditor guide + +This page is for someone reviewing ISEDRAF evidence they did not produce. It explains what +to look at first, how to check a single fact by hand without trusting ISEDRAF, and what +the evidence cannot tell you. + +## What you are given + +Usually two things: a report (HTML, Markdown or JSON), and a copy of the operator's +evidence store, or at least the run directory the report names. The report is a rendering; +the files in the run are the evidence. When they disagree, the files win, and the +disagreement is itself worth recording. + +Everything in a run is plain JSON. You need no ISEDRAF installation to read or check it; +Python's standard library is enough. + +## Five things to read first + +1. **Privilege level.** ISEDRAF 0.1 always reports UNPRIVILEGED. Anything that needs root + was not observed. +2. **Overall evidence status.** COLLECTED, PARTIAL or ERROR. PARTIAL is normal for an + unprivileged run. +3. **Evidence class.** USER_PRODUCTION is a normal run. DEV is a development run and is not + production evidence. +4. **Verification line.** The report states whether the digests, bindings and ledger chain + held when it was rendered. +5. **Each area's status and reason.** A reason tells you exactly what was not read and why, + for example `SOURCE_UNREADABLE: /etc/sudoers could not be read.` + +The [Evidence model](EVIDENCE_MODEL.md) explains the statuses; the +[Report guide](REPORT_GUIDE.md) walks through the report layout. + +## Checking one observation by hand + +Each report section ends with an evidence reference: the file inside the run and its +digest. The steps below check that reference, the run that contains it, and the ledger +entry that commits the run, using only Python. The values are from a small test host. + +The report said, for local accounts: + +```text +evidence: sections/accounts.json · sha256:b78bea64006d0219e1abc0c87a3fa6c9d3023cb4668c0c72ee5450d8d879f8c3 +``` + +ISEDRAF digests are SHA-256 over a fixed label, the content length as 8 bytes, and the +content. The label keeps a file digest from ever being mistaken for any other kind of +digest. That is why a plain `sha256sum` gives a different value. + +**Step 1. Recompute the file's digest.** Run this in the run directory +(`snapshots/SDS-…/` in the store): + +```sh +python3 - sections/accounts.json <<'PY' +import hashlib, sys +data = open(sys.argv[1], "rb").read() +label = b"ISEDRAF:AUXILIARY-ARTIFACT:V1" +print("sha256:" + hashlib.sha256(label + len(data).to_bytes(8, "big") + data).hexdigest()) +PY +``` + +It must print the digest the report showed. + +**Step 2. Check that the run lists that digest.** + +```sh +python3 -c 'import json; print(json.load(open("manifest.json"))["manifest_core"]["auxiliary_artifacts"]["sections/accounts.json"])' +``` + +**Step 3. Recompute the manifest's own digest.** The manifest holds only identifiers and +digests, so sorted, compact JSON reproduces ISEDRAF's canonical form exactly. + +```sh +python3 - <<'PY' +import hashlib, json +m = json.load(open("manifest.json")) +core = (json.dumps(m["manifest_core"], sort_keys=True, separators=(",", ":"), + ensure_ascii=False) + "\n").encode("utf-8") +label = b"ISEDRAF:SNAPSHOT-MANIFEST:V1" +print("sha256:" + hashlib.sha256(label + len(core).to_bytes(8, "big") + core).hexdigest()) +print(m["manifest_hash"]) +PY +``` + +The two lines must be equal. + +**Step 4. Check the ledger.** Every record must hash correctly and point at the record +before it; the record for this run must carry the manifest digest from step 3. + +```sh +python3 - ../../ledger/segment-000001.jsonl <<'PY' +import hashlib, json, sys +previous = "sha256:" + "0" * 64 +for line in open(sys.argv[1]): + r = json.loads(line) + core = (json.dumps(r["record_core"], sort_keys=True, separators=(",", ":"), + ensure_ascii=False) + "\n").encode("utf-8") + label = b"ISEDRAF:LEDGER-RECORD:V1" + ok = ("sha256:" + hashlib.sha256(label + len(core).to_bytes(8, "big") + core).hexdigest() + == r["record_hash"] and r["record_core"]["previous_record_hash"] == previous) + print(r["record_core"]["sequence"], r["record_core"]["snapshot_id"], + r["record_core"]["manifest_hash"], "ok" if ok else "MISMATCH") + previous = r["record_hash"] +PY +``` + +**Step 5. Read the fact itself.** The report said "Accounts with uid 0: 1". Count it in +the evidence: + +```sh +python3 -c 'import json; e=json.load(open("sections/accounts.json"))["evidence"]; print(sum(1 for a in e["local_accounts"] if a["uid"] == 0))' +``` + +If you have access to the host, compare with the source file, for example +`awk -F: '$3 == 0' /etc/passwd`. A difference may simply mean the host changed after the run. + +## What the evidence does not tell you + +These limits hold for every run. They are not caveats to skip. + +- **An accepted baseline is not a secure host.** When baselines arrive, accepting one will + record a decision, not a security judgement. ISEDRAF 0.1 has no baselines at all. +- **No observed change does not mean uncompromised.** ISEDRAF reads what userspace and the + kernel expose. Someone with root can alter those sources, the logs, the evidence store + and ISEDRAF itself. +- **NOT_TESTED is not PASS.** It means the fact was not observed. Nothing may be inferred + from it in either direction. +- **Host evidence is not organizational compliance.** A run describes one host at one + moment. It says nothing about policies, people or processes, and no framework mapping + exists in this repository. +- **A missing local product is not a missing control.** If no firewall, antivirus or EDR + product is seen on the host, the control may still exist outside it. +- **This is not remote attestation.** Verification shows that the files match what the + ledger recorded. It does not show that the host reported the truth, and nothing in a run + is signed. + +[Security and limitations](SECURITY_AND_LIMITATIONS.md) lists everything outside the +scope of ISEDRAF 0.1. + +## Questions worth asking the operator + +- Was this run made on the host it names, as a normal user, without `ISEDRAF_STATE_ROOT`? +- Are there later runs, and why was this one chosen? +- Who else can read or write the evidence store, and who has root on the host? +- Which areas were NOT_TESTED, and is there other evidence for them? + +## References + +Normative sources, for readers who need them: hash framing and canonical form +(NORM-035, NORM-039); section binding outside the host-state hash (D-115); commit order and +the ledger (SNAP-014, D-50); collection versus evaluation (EVID-001, D-13); the +limitations above (CMP-001, CMP-002, EVID-040). The frozen design is under +[docs/architecture](architecture/). diff --git a/docs/CURRENT_STATE.md b/docs/CURRENT_STATE.md index 10be720..9e5ea3a 100644 --- a/docs/CURRENT_STATE.md +++ b/docs/CURRENT_STATE.md @@ -17,7 +17,7 @@ version drifted until it announced that no product code existed while three comm | | | |---|---| -| Project stage | **TECHNICAL_PREVIEW_CANDIDATE** | +| Project stage | **GENERAL_AVAILABILITY** | | Public release | **AWAITING_OWNER_AUTHORIZATION** | | Production Python floor | 3.6 | | Tooling Python floor | 3.9 | @@ -111,15 +111,14 @@ Not asserted. Each number is counted at generation time. | | | |---|---| -| Gates | 20 | -| Falsification injections | 113 | +| Gates | 28 | +| Falsification injections | 372 | | Golden vector cases | 15 | | Frozen artifacts | 7 | -| Test files | 3 | +| Test files | 33 | ## Release blockers The public repository is **not** authorized while any of these is open. -- an external integration boundary is under review; the Technical Preview is not released until the required repository-isolation condition is verified - no GitHub Release or tag is published; publication is a separate owner-authorized act (D-110) diff --git a/docs/DEPENDENCIES.md b/docs/DEPENDENCIES.md new file mode 100644 index 0000000..d4454d0 --- /dev/null +++ b/docs/DEPENDENCIES.md @@ -0,0 +1,52 @@ + +# Dependencies + +Status: IMPLEMENTED +Implements: D-12, D-84, D-86 + +How ISEDRAF chooses, obtains, pins, monitors and updates what it depends on. + +## At run time + +ISEDRAF 0.1 needs the host's own **Python 3.6 or later** and nothing else from Python: it uses the +**standard library only**. There are no third-party Python modules, no compiled components and no +plugins. + +- The package declares a dependency on the distribution's Python (`python3`, or `platform-python` on + Enterprise Linux 8). ISEDRAF never installs, bundles, downgrades or replaces the host interpreter. +- Every module the product imports is on a fixed allowlist, and `make check` fails on any import that is + not. A new runtime dependency is therefore a reviewed change to that allowlist, not an accident. +- External programs the collectors may call are ordinary system tools found at run time. Where one is + missing, the affected evidence is reported as `NOT_TESTED` with the reason; nothing is installed. + +## For development and packaging + +There are no third-party Python packages for development either. The checks behind `make check` are +written with the standard library and use system tools: `git`, `make`, `bash`, GNU coreutils, and, +where available, `shellcheck`, `rsync`, `dpkg-deb` and `rpmbuild`. These tools are never imported by +product code. + +## In CI + +- GitHub Actions are the only third-party code CI runs. Each is **pinned to a full commit SHA**, never a + tag or branch, and `make check` fails on an unpinned action. +- Workflows start with no token permissions and grant each job only what it needs. +- The SBOM attached to each release is generated by the project's own build script. + +## Selecting a dependency + +A dependency is added only when the standard library or a system tool cannot reasonably do the job. +It is proposed in a pull request that explains why it is needed, what it can access, and who maintains +it. Unnecessary dependencies are rejected. A runtime dependency beyond the standard library would change +the project's supply-chain model and needs the maintainer's explicit decision. + +## Monitoring and updating + +- **Dependabot** monitors the GitHub Actions used by the workflows and opens pull requests for updates. + Dependabot alerts and security updates are enabled on the repository. +- An update is merged only through a pull request that passes every required check, like any other + change, and keeps the full-SHA pinning. +- The host Python and system tools are updated by the host's distribution, not by ISEDRAF. diff --git a/docs/EVIDENCE_MODEL.md b/docs/EVIDENCE_MODEL.md new file mode 100644 index 0000000..f9152f8 --- /dev/null +++ b/docs/EVIDENCE_MODEL.md @@ -0,0 +1,136 @@ + +# Evidence model + +This page explains what ISEDRAF records, what each status means, and why a run can be +trusted as a record of what was collected. It describes ISEDRAF 0.1. + +## Collecting is not judging + +ISEDRAF records what it saw and whether it could see it. It never decides whether what it +saw is good or bad. There is no PASS, FAIL, score or finding in 0.1. + +A fact that could not be observed is reported as not observed, never as absent and never +as fine. That rule decides almost everything else on this page. + +## One run, committed once + +`isedraf audit` collects every area once and commits the result as one **run**. A run is a +directory under `snapshots/` in your evidence store: + +```text +snapshots/SDS-