ndaal Information Advisory: update_ansible_role.sh hardening — version-drift fix + section-aware Cargo.toml fetch + 5-sidecar contract on both Ansible roles

Edit JSON

Metadata

Tracking IDndaal-sa-2026-077
Categorycsaf_informational_advisory
CSAF Version2.1
Statusfinal
Version1.0.0
Publisherndaal Gesellschaft fuer Sicherheit in der Informationstechnik mbH & Co KG
TLPCLEAR
Initial Release2026-05-26T00:00:00.000Z
Current Release2026-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"
                }
              }
            ]
          }
        ]
      }
    ]
  }
}

Are you sure?