{"$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"}}]}]}]}}