ndaal Information Advisory: update_ansible_role.sh hardening — version-drift fix + section-aware Cargo.toml fetch + 5-sidecar contract on both Ansible roles
Metadata
| Tracking ID | ndaal-sa-2026-077 |
|---|---|
| Category | csaf_informational_advisory |
| CSAF Version | 2.1 |
| Status | final |
| Version | 1.0.0 |
| Publisher | ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG |
| TLP | CLEAR |
| Initial Release | 2026-05-26T00:00:00.000Z |
| Current Release | 2026-05-26T00:00:00.000Z |
Vulnerabilities (0)
No vulnerabilities.
Raw JSON
{
"$schema": "https://docs.oasis-open.org/csaf/csaf/v2.1/schema/csaf.json",
"document": {
"category": "csaf_informational_advisory",
"csaf_version": "2.1",
"distribution": {
"tlp": {
"label": "CLEAR"
}
},
"lang": "en",
"notes": [
{
"category": "summary",
"text": "vulnerability-lookup-rs/scripts/update_ansible_role.sh has been hardened to close a version-drift loop and to refuse silently-malformed input. Two complementary defects, one operational follow-up:\n\n * BUG A (operational drift, surfaced by operator deploy 2026-05-26):\n The role-files-refresh script propagated the workspace `VL_VERSION`\n (resolved from `Cargo.toml`) only into binary copies and per-binary\n sidecars, NEVER into the embedded `nvulnlookup_version:` /\n `SCRIPT_VERSION='...'` strings scattered across the Ansible tree\n (playbook role-invocation override, both role defaults, eight Jinja2\n fallback defaults inside install_from_package.yml, three macOS\n bootstrap scripts). An operator bumping 0.1.44 → 0.1.48 saw the\n role's files/ tree ship the new 0.1.48 packages but the playbook\n still pin `nvulnlookup_version: 0.1.44`, the install task resolve\n `vl-cli_0.1.44-1_*.deb` (a no-longer-existing filename), the install\n silently no-op, the prior 0.1.48 binary keep running, and\n `verify_deployed_version.yml` correctly catch the running-vs-declared\n mismatch with the misleading 'Running 0.1.48 but 0.1.44 was\n deployed' assertion.\n\n * BUG B (fragile Cargo.toml fetch, latent):\n Version detection was a positional `grep -m1 '^version[[:space:]]*='`\n that grabbed the first version line anywhere in the workspace\n Cargo.toml. Any future `[workspace.metadata.foo]` table with its\n own `version = '...'` field would silently poison `VL_VERSION` and\n propagate the wrong value across every consuming artefact.\n\n * KNOWN follow-up (filed):\n `sed_in_place()` does not preserve the source file's mode when\n overwriting via the temp-file dance, stripping +x off the three\n macos_*_install.sh scripts under `umask 077`. Mitigated in the\n current release by chmod +x post-hoc; root cause will land in a\n follow-up patch."
},
{
"category": "description",
"text": "Detail per bug:\n\n1. BUG A — version drift across the Ansible tree:\n The script's prior shape touched only the binary + package + per-\n binary sidecar tree. 14 embedded version-string references in 8\n files were maintained by hand: tools/ansible/playbooks/\n deploy_nvulnlookup.yml (line 10), both role defaults/main.yml,\n both tasks/install_from_package.yml (4 Jinja2 fallback defaults\n each — Debian + RHEL × web + cli), and the three macOS bootstrap\n scripts under nvulnlookuptesting/scripts/. A release-bump cycle\n therefore required hand-editing 14 anchor points; forgetting any\n one of them produced the silent install no-op and the misleading\n post-deploy assert failure documented above. Fix landed in\n commits a73eb5b65 (manual 13-reference bump 0.1.44 → 0.1.48 to\n unblock the immediate operator) + 3ac2a0753 (new helper\n `sed_in_place(pattern, file)` + new function\n `propagate_version_to_ansible_artefacts()` wired into main() AFTER\n the role-iteration loop) + ad22f4695 (Cargo.toml fetch hardening +\n bats extension to 45/45) + e07191e70 (5-sidecar contract backfill\n on both role files/ + molecule trees).\n\n2. BUG B — Cargo.toml fetch was positional, not section-aware:\n Replaced the positional `grep -m1` with a dedicated\n `fetch_workspace_version()` function that uses awk to enter\n extraction mode only inside the literal `[workspace.package]`\n section header. Returns empty when the section is absent, when\n the file is missing, or when awk itself is unavailable (defensive\n fallback to the legacy grep+sed shape). Two guards bracket the\n call: an empty-result guard (hard exit 1 with explanatory message\n pointing at the missing [workspace.package] anchor) and a semver\n shape check (regex `^[0-9]+\\.[0-9]+\\.[0-9]+([-+][A-Za-z0-9.-]+)?$`)\n that refuses junk before it can propagate into 14 ansible-tree\n references.\n\n3. PROPAGATOR escape hatch:\n `VL_SKIP_VERSION_PROPAGATION=1` short-circuits the propagator\n sweep before any sed runs. For release-rehearsal scenarios where\n the version strings are deliberately out of phase.\n\n4. Bats coverage:\n Sister `update_ansible_role.bats` extended from 31 to 45 tests\n (per skills/bats canonical conventions). Added 14 targeted tests\n (32-45) covering: function existence, section-aware awk pattern,\n real Cargo.toml fetch, section-aware behaviour (synthetic fixture\n with misleading earlier version line), failure-loud-on-missing-\n section, semver guard, sed_in_place existence + portability + idem-\n potency, propagator existence + 8-file coverage + main()-ordering,\n VL_SKIP_VERSION_PROPAGATION knob, end-to-end idempotency, and\n short-circuit behaviour.\n\n5. 5-sidecar contract:\n Both Ansible roles' files/ + molecule/default/files/ trees were\n short two sidecar families (blake3-512 + shake256-512) per\n CLAUDE.md 'Cryptographic hashes'. Backfill ran via\n `vulnerability-lookup-rs/scripts/backfill_5sidecar_contract.sh`:\n added 28 per-file sidecars, verified 52, zero failures."
},
{
"category": "details",
"text": "Verification (commits a73eb5b65 / 3ac2a0753 / ad22f4695 / e07191e70):\n\n 1. Lint sweep on the edited script:\n bash -n vulnerability-lookup-rs/scripts/update_ansible_role.sh # rc=0\n shfmt -d -i 0 -bn -ci -sr vulnerability-lookup-rs/scripts/update_ansible_role.sh # rc=0\n shellcheck -o all vulnerability-lookup-rs/scripts/update_ansible_role.sh # rc=0\n\n 2. Sister bats (extended to 45 tests):\n bats vulnerability-lookup-rs/scripts/update_ansible_role.bats # 45/45 ok\n\n 3. Functional unit tests of the new helpers (synthetic Cargo.toml\n fixtures, idempotency, escape-hatch):\n fetch_workspace_version against real Cargo.toml -> 0.1.48 (correct)\n fetch_workspace_version against synthetic toml with -> 0.1.48 (ignored 99.99.99 in\n a 99.99.99 in [workspace.metadata.foo] [workspace.metadata.foo])\n fetch_workspace_version against missing file -> empty + caller exits 1\n fetch_workspace_version against sectionless toml -> empty + caller exits 1\n propagator drift sweep (faked 0.1.44 → 0.1.48) -> 8/8 files bumped, 0 stale refs remain\n propagator idempotent re-run on converged tree -> 8/8 no-op, changed=0 missing=0\n VL_SKIP_VERSION_PROPAGATION=1 with bogus VL_VERSION -> clean short-circuit, no rewrites\n\n 4. CSAF advisory validation (this document):\n scripts/validate_csaf_documents.sh --root csaf/2026/077 # Passed: 1\n csaf/generate_distribution_index.sh # index.txt + changes.csv refreshed\n\n 5. Operational replay of the original deploy failure:\n ansible-playbook tools/ansible/playbooks/deploy_nvulnlookup.yml\n -> install task resolves to existent vl-cli_0.1.48-1_*.deb in the role's files/ tree\n -> binary copied + service restarted\n -> verify_deployed_version.yml assertion passes ('Running == nvulnlookup_version' both 0.1.48)\n\nUpgrade path: drop-in. Operators on the existing repo just pull the\nlatest main and re-run their normal deploy cycle. No configuration\nchange is required. Next workspace version bump (0.1.48 → 0.1.49,\netc.) will benefit transparently — running `update_ansible_role.sh`\nafter a clean build automatically propagates the new version into all\n14 ansible-tree references."
},
{
"category": "general",
"text": "Recommended action: pull main and run `bash vulnerability-lookup-rs/scripts/update_ansible_role.sh` exactly once on any environment that maintains its own checkout of this repository. The script is idempotent on a converged tree (the propagator's `cmp -s` short-circuit reports `changed=0 missing=0` and does not bump mtime on any file), so re-running it as part of a release-rehearsal flow is safe. Operators who deliberately want to ship a version-string-skewed test build can pass `VL_SKIP_VERSION_PROPAGATION=1` to bypass the new propagator while keeping the binary-and-sidecar refresh behaviour."
},
{
"category": "other",
"text": "Severity classification: this advisory documents an OPERATIONAL drift defect (Bug A — version-string out-of-sync across 14 Ansible-tree anchors caused deploy assert failure) plus a LATENT correctness defect (Bug B — positional Cargo.toml fetch could be poisoned by a future workspace metadata table). Neither maps to a CVE. Neither has known exploitation potential — the failure mode is deploy-time + observably broken (post-deploy assert fires loud), not silent compromise. The advisory is informational and is published so downstream consumers + SBOM tooling can trace the operational-tooling change between commit 7bed605ba (release 0.1.47) and commit e07191e70 (this 0.1.48 follow-up). Status: final. A separate task chip has been filed for the remaining sed_in_place() mode-preservation defect."
},
{
"category": "legal_disclaimer",
"text": "THIS DOCUMENT IS PROVIDED ON AN 'AS IS' BASIS AND DOES NOT IMPLY ANY KIND OF GUARANTEE OR WARRANTY. ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS INFORMATION INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS."
}
],
"publisher": {
"category": "vendor",
"contact_details": "security@ndaal.eu",
"issuing_authority": "ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG Security Team",
"name": "ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG",
"namespace": "https://ndaal.eu/csaf"
},
"references": [
{
"category": "self",
"summary": "This advisory in CSAF 2.1 format",
"url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/raw/main/csaf/2026/077/ndaal-sa-2026-077.json"
},
{
"category": "external",
"summary": "Commit a73eb5b65 — manual 13-reference bump 0.1.44 → 0.1.48",
"url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/commit/a73eb5b65"
},
{
"category": "external",
"summary": "Commit 3ac2a0753 — new propagate_version_to_ansible_artefacts() function",
"url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/commit/3ac2a0753"
},
{
"category": "external",
"summary": "Commit ad22f4695 — Cargo.toml fetch hardened (section-aware) + bats extended to 45/45",
"url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/commit/ad22f4695"
},
{
"category": "external",
"summary": "Commit e07191e70 — 5-sidecar contract backfill on both role + molecule trees",
"url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/commit/e07191e70"
},
{
"category": "external",
"summary": "update_ansible_role.sh — release-pipeline helper",
"url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/blob/main/vulnerability-lookup-rs/scripts/update_ansible_role.sh"
},
{
"category": "external",
"summary": "update_ansible_role.bats — sister test file (45 tests)",
"url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/blob/main/vulnerability-lookup-rs/scripts/update_ansible_role.bats"
},
{
"category": "external",
"summary": "CLAUDE.md — Cryptographic hashes chapter (5-sidecar contract)",
"url": "https://gitlab.com/vPierre/ndaal_public_nvulnlookup/-/blob/main/CLAUDE.md"
}
],
"title": "ndaal Information Advisory: update_ansible_role.sh hardening — version-drift fix + section-aware Cargo.toml fetch + 5-sidecar contract on both Ansible roles",
"tracking": {
"current_release_date": "2026-05-26T00:00:00.000Z",
"generator": {
"engine": {
"name": "ndaal CSAF Generator",
"version": "1.0.0"
}
},
"id": "ndaal-sa-2026-077",
"initial_release_date": "2026-05-26T00:00:00.000Z",
"revision_history": [
{
"date": "2026-05-26T00:00:00.000Z",
"number": "1.0.0",
"summary": "Initial informational advisory documenting two defects fixed across commits a73eb5b65 / 3ac2a0753 / ad22f4695 / e07191e70 in vulnerability-lookup-rs/scripts/update_ansible_role.sh. Bug A (operational drift): the script propagated the workspace version into binaries + sidecars but not into the 14 embedded `nvulnlookup_version` / `SCRIPT_VERSION='...'` references across the Ansible tree (playbook + 2 role defaults + 8 Jinja2 fallback defaults + 3 macOS bootstrap scripts), causing the 0.1.44 → 0.1.48 deploy to fail the post-deploy version-assert with the misleading message 'Running 0.1.48 but 0.1.44 was deployed'. Bug B (latent correctness): positional `grep -m1 '^version'` could be poisoned by a future workspace metadata table. Fixes: new `propagate_version_to_ansible_artefacts()` function wired into main() (Bug A), new `fetch_workspace_version()` with section-aware awk + semver guard (Bug B), sister bats extended to 45/45 covering both new helpers + the escape hatch. 5-sidecar contract (sha-256/sha-512/sha3-512/blake3-512/shake256-512) now complete on both Ansible roles' files/ + molecule trees. Status: final."
}
],
"status": "final",
"version": "1.0.0"
}
},
"product_tree": {
"branches": [
{
"category": "vendor",
"name": "ndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG",
"branches": [
{
"category": "product_name",
"name": "update_ansible_role.sh",
"branches": [
{
"category": "product_version",
"name": "pre-a73eb5b65",
"product": {
"name": "vulnerability-lookup-rs/scripts/update_ansible_role.sh prior to commit a73eb5b65 — copies binaries + sidecars but does not propagate VL_VERSION into the 14 embedded `nvulnlookup_version` / `SCRIPT_VERSION='...'` references across the Ansible tree; Cargo.toml fetch is a positional `grep -m1`.",
"product_id": "CSAFPID-0001"
}
},
{
"category": "product_version",
"name": "post-e07191e70",
"product": {
"name": "vulnerability-lookup-rs/scripts/update_ansible_role.sh after commit e07191e70 — section-aware Cargo.toml fetch via awk + semver guard; new `propagate_version_to_ansible_artefacts()` rewrites 14 embedded references in 8 files via portable sed-in-place; `VL_SKIP_VERSION_PROPAGATION=1` escape hatch; 5-sidecar contract complete on both roles' files/ + molecule trees; sister bats covers 45/45 (was 31/31).",
"product_id": "CSAFPID-0002"
}
}
]
}
]
}
]
}
}