Skip to content

ci: add pinned security scanning workflows - #6

Merged
Mushtaq-BGA merged 4 commits into
release-1.0from
feat/security-scans
Sep 3, 2026
Merged

Mushtaq-BGA merged 4 commits into
release-1.0from
feat/security-scans

Conversation

@Mushtaq-BGA

@Mushtaq-BGA Mushtaq-BGA commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Adds security scanning to release-1.0, following the pattern already used across the intel org: per-tool workflow files, harden-runner as the first step, OpenSSF Scorecard, and actions pinned by commit SHA.

Workflow Tool Covers SARIF
trivy.yml Trivy vulnerabilities, secrets, misconfig, licenses yes
bandit.yml Bandit Python security findings yes
checkov.yml Checkov Kubernetes / Actions / secrets yes
shellcheck.yml ShellCheck shell correctness & quoting annotations
ansible-lint.yml ansible-lint Ansible correctness annotations
scorecard.yml OpenSSF Scorecard supply-chain posture yes

Each tool uploads under its own category:, so results don't overwrite one another.

Pinning

Every action is pinned to a full commit SHA (tag in a trailing comment) and every tool to an exact version — no floating latest/major tags.

Actions: harden-runner v2.21.1 · checkout v7.0.1 · setup-python v7.0.0 · upload-artifact v4.6.2 · trivy-action v0.36.0 · codeql-action/upload-sarif v4.37.9 · scorecard-action v2.4.4

Tools: Python 3.12.14 · Bandit 1.9.4 · Checkov 3.3.16 · ansible-lint 26.8.0 · Trivy CLI v0.74.0 · ShellCheck v0.11.0 · runner ubuntu-24.04

The SHAs for checkout and trivy-action match those already in use in intel/MigTD, intel/cve-bin-tool and intel/intel-device-plugins-for-kubernetes.

ShellCheck is fetched from its upstream release and checksum-verified rather than installed via apt, whose version drifts with the runner image. Trivy's binary is pinned but its vulnerability DB is fetched fresh each run — intentional, since a scanner pinned to stale CVE data is worse than useless.

Two tools were retuned after running them locally

Both scans were run against this tree before landing, which surfaced problems that would have made them useless in practice:

Checkov drops the helm framework. It renders charts into a random temp dir and reports paths relative to that, so all findings pointed at files like tmppna2ze0v/charts/.../deployment.yaml — nonexistent in the repo, therefore unanchorable in the Security tab, and re-reported as brand-new on every run since the temp name changes. The findings were also entirely inside vendored upstream charts. Trivy's misconfig scanner covers chart misconfiguration with correct paths, so coverage isn't lost. charts/ is skipped for the same reason.

ansible-lint skips style-only rule families. Unfiltered it produced 502 findings here, of which just 6 were substantive — name[casing] alone fired against the deliberate role | action | desc naming convention. Skipping name,var-naming,fqcn,yaml,jinja,key-order leaves exactly the correctness and command-shell findings.

dependency-review.yml is not included: this repo has no dependency manifest (no requirements.txt/pyproject.toml anywhere), so the check would be a permanently-passing no-op. Worth adding the day one lands. Note this also means Trivy's vuln scanner has no Python manifest to resolve, so a green Trivy result should not be read as "no Python dependency risk" for this repo.

Hardening

  • Top-level permissions: contents: read; security-events: write only where SARIF is uploaded.
  • persist-credentials: false on every checkout.
  • concurrency groups cancel superseded runs.
  • GitHub-hosted runners and public actions only — no internal infrastructure referenced.

Notes

  • Soft-fail for this first rollout (exit-code: '0', --exit-zero, --soft-fail, continue-on-error). Nothing blocks the branch yet, so the backlog can be triaged before any check is made required.
  • CodeQL is deliberately not added — it already runs here via code scanning default setup; a workflow file would require disabling that first.
  • scorecard.yml triggers on the default branch, branch_protection_rule and a schedule, not on PRs — it grades the repository, not a diff. It will not appear in this PR's checks; verify it via workflow_dispatch after merge.
  • Scorecard's publish_results: true sends results to OpenSSF's public dataset (this is what the other 24 Scorecard-using intel repos do). Set it to false to keep results in the Security tab only.
  • Scorecard's Branch-Protection check needs an admin-scoped token and will read as inconclusive with the default GITHUB_TOKEN.

Test plan

Verified locally before pushing:

  • actionlint 1.7.12 + shellcheck 0.11.0 clean across all 6 workflow files (includes linting the run: blocks).
  • All 8 pinned SHAs re-verified live to resolve to their claimed tags.
  • All 8 trivy-action inputs confirmed to exist in action.yaml at the pinned SHA.
  • ShellCheck tarball SHA256 confirmed by downloading the release and running sha256sum --check --strict.
  • Bandit / Checkov / ansible-lint versions confirmed present on PyPI; Python 3.12.14 confirmed in the actions/python-versions manifest with a 24.04 linux-x64 build.
  • Trivy CLI v0.74.0 release confirmed to exist.
  • Bandit run locally → valid SARIF 2.1.0, 0 findings (repo has only 2 Python files).
  • Checkov run locally with the final flags → results.sarif produced, 0 findings with 172 checks passing.
  • ShellCheck run locally → 33 warnings across 7 scripts, mostly SC2034 in sourced lib files (non-blocking).
  • ansible-lint run locally with the final flags → 6 findings (2 schema, 3 no-changed-when, 1 risky-shell-pipe).
  • Confirmed clean merge into release-1.0 with no base drift; commit touches only .github/workflows/.
  • Confirmed no internal hosts or resources referenced; sole external download endpoint is github.com/koalaman/shellcheck/releases.

To confirm on CI:

  • All jobs complete on this PR.
  • Findings appear under Security → Code scanning, split by category.
  • scorecard.yml dispatched successfully after merge.

Second commit: false-positive triage (813d996)

Every finding the scans produced was triaged by hand, separating defects that
cannot be true from ones worth a maintainer's time.

Scan Before After
ShellCheck 33 7
Trivy 17 1
Bandit 0 0
Checkov 0 0
ansible-lint 4 4

ShellCheck. Discovery now reads shebangs as well as *.sh suffixes:
model_manager/model-manager is the largest script in the tree at 1429 lines
and a *.sh glob never saw it. Entry points are linted with -x so the helper
scripts they source are followed, plus a --source-path list — the source
statements are built from "$MM_LIB/...", which ShellCheck cannot resolve
unaided, and without it 7 globals that model_manager/lib/ does set and
model-manager does read looked unused.

The helper scripts under model_manager/lib/ are linted on their own.
ShellCheck sees one file at a time, so a global a helper sets for its caller
reads as unused: 26 of the 31 SC2034 hits were that. Disabling SC2034 for
those files was too blunt — it also hid four globals that genuinely are written
and never read anywhere. Each hit is now checked individually and suppressed
only when another file demonstrably reads that variable, limited to
SHOUTY_CASE names of 3+ characters, since a short lowercase name such as a
loop's i collides textually with unrelated files.

No file under model_manager/ was modified.

Trivy — a real defect fixed. limit-severities-for-sarif defaults to
false, and left unset the action drops --severity for the SARIF run alone: the
job's table printed 0 findings while the uploaded SARIF carried all 17 at
LOW/MEDIUM, every one below the declared CRITICAL,HIGH threshold. The Security
tab was silently ignoring the configured policy. Now split in two — CVEs,
secrets and licences at CRITICAL,HIGH with the limit applied, and
misconfiguration at every severity, because Trivy rates nearly every Kubernetes
check LOW or MEDIUM and the same threshold would silence that scanner outright.
Noise is controlled by scope instead: vendored charts/litellm-helm accounted
for 16 of the 17 and is now skipped, matching Checkov.

Bandit. Skips B404 (importing subprocess is not a defect) and B603,
which fires on the recommended argv-list form and cannot judge whether the
input is trusted. shell=True and friends (B602/B604/B605/B609) stay
enabled. Severity is deliberately not filtered, since real tests such as
B105 report LOW.

Findings deliberately kept — each verified against the whole tree

  • config.sh:266 — RUNTIME_DEVICE is parsed from the runtime JSON
    (.device // "cpu") beside five siblings that are all consumed, then
    never read. Device selection is read and dropped.
  • nri.sh:82,83 — NRI_ADVANCED_MODE and NRI_HIDE_HT are both named in the
    function's own Sets: header comment as outputs, and neither is consumed.
    The Python side reads the MM_NRI_HIDE_HT environment variable, not these.
  • nri.sh:241 — NRI_TARGET_NODE, set beside NRI_POLICY_NAME, which is
    used.
  • helpers.sh:244 — local i in a for i in $(seq 1 24) retry loop whose body
    never uses it.
  • helpers.sh:88, routing.sh:288 — SC2155.
  • KSV-0106 in roles/llm_services/files/runtimes/vllm-runtime.yaml, the one
    Trivy finding in code this repo owns.
  • All 4 ansible-lint findings.

These are reported, not fixed — all scans are soft-fail, so they surface without
blocking.

Verified on CI

  • All 5 workflows pass on this PR.
  • ShellCheck reports 8 script(s): 3 entry point(s), 5 sourced helper(s)
    and 7 findings — matching the local run exactly.
  • SARIF ingested under the new split categories: trivy-misconfig n=1
    (down from trivy n=17), trivy-vuln n=0, bandit n=0, checkov n=0.

Known gaps, left open by choice

  • Vendored charts/ is now scanned by neither Checkov nor Trivy. Worth
    revisiting if those charts are ever forked rather than vendored.
  • This repo has no dependency manifest, so Trivy's vuln scanner has nothing to
    resolve — a green Trivy result must not be read as "no Python dependency
    risk". This is also why there is no dependency-review workflow here.
  • No .github/dependabot.yml, which scores Scorecard's Dependency-Update-Tool
    check at 0.
  • scorecard.yml does not run on pull requests and still needs a
    workflow_dispatch after merge to confirm it works.

Add SARIF-publishing security scans for the release branch, following the
pattern already used across the intel org (harden-runner + per-tool
workflow files + OpenSSF Scorecard):

- trivy       vulnerabilities, secrets, misconfig, licenses (filesystem)
- bandit      Python security findings
- checkov     Kubernetes / Actions / secrets IaC checks
- shellcheck  shell script correctness and quoting
- ansible-lint Ansible role and playbook hygiene
- scorecard   OpenSSF supply-chain posture rating

Every action is pinned to a full commit SHA and every tool to an exact
version, so a given commit always scans the same way and version bumps are
explicit, reviewable diffs. ShellCheck is fetched from its upstream release
and checksum-verified rather than installed via apt, whose version drifts
with the runner image.

Each scan was run locally against the tree before landing, and two were
retuned as a result:

- checkov drops the `helm` framework. Checkov renders charts into a random
  temp dir and reports paths relative to it, so all 50 (ai-solutions) / 40
  (inference) findings pointed at files that do not exist in the repo --
  unanchorable in the Security tab and re-reported as new every run. They
  were also entirely in vendored upstream charts. Trivy's misconfig scanner
  covers chart misconfiguration with correct paths.
- ansible-lint skips style-only rule families, which accounted for 1158 of
  1163 findings in ai-solutions and 496 of 502 in inference. name[casing]
  alone fired 350 times against the deliberate `role | action | desc`
  naming convention.

Jobs are soft-fail for the initial rollout: findings surface under
Security -> Code scanning instead of blocking the branch, so the existing
backlog can be triaged before any check is made required.

CodeQL is intentionally not added here - it already runs via code scanning
default setup on both repositories.
@github-advanced-security

Copy link
Copy Markdown

You are seeing this message because GitHub Code Scanning has recently been set up for this repository, or this pull request contains the workflow file for the Code Scanning tool.

What Enabling Code Scanning Means:

  • The 'Security' tab will display more code scanning analysis results (e.g., for the default branch).
  • Depending on your configuration and choice of analysis tool, future pull requests will be annotated with code scanning analysis results.
  • You will be able to see the analysis results for the pull request's branch on this overview once the scans have completed and the checks have passed.

For more information about GitHub Code Scanning, check out the documentation.

Triage of every finding the scans produced, separating defects that cannot be
true from ones worth a maintainer's time. Every remaining finding was checked by
hand against the source and is real.

ShellCheck (33 -> 7)
  Discovery now reads shebangs as well as *.sh suffixes: model_manager/model-
  manager is the largest script in the tree at 1429 lines and a *.sh glob never
  saw it.

  Entry points are linted with -x so the helper scripts they source are
  followed, plus a --source-path list -- the source statements are built from
  "$MM_LIB/..." which ShellCheck cannot resolve unaided, and without it 7
  globals that model_manager/lib/ does set and model-manager does read looked
  unused.

  The helper scripts under model_manager/lib/ are linted on their own.
  ShellCheck sees one file at a time, so a global a helper sets for its caller
  reads as unused: 26 of the 31 SC2034 hits were that. Disabling SC2034 for
  those files was too blunt -- it also hid four globals that genuinely are
  written and never read anywhere. Each hit is now checked individually and
  suppressed only when another file demonstrably reads that variable, limited to
  SHOUTY_CASE names of 3+ characters since a short lowercase name such as a
  loop's `i` collides textually with unrelated files.

  Kept, all verified by grepping the whole tree:
  - config.sh:266  RUNTIME_DEVICE, parsed from the runtime JSON beside five
    siblings that are all consumed, then never read -- cpu/gpu selection is
    read and dropped.
  - nri.sh:82,83   NRI_ADVANCED_MODE and NRI_HIDE_HT, both named in the
    function's own "Sets:" header comment as outputs, neither consumed. The
    Python side reads the MM_NRI_HIDE_HT environment variable, not this.
  - nri.sh:241     NRI_TARGET_NODE, set beside NRI_POLICY_NAME which is used.
  - helpers.sh:244 `local i` in a `for i in $(seq 1 24)` retry loop whose body
    never uses it.
  - helpers.sh:88, routing.sh:288  SC2155.

  No file under model_manager/ was modified.

Bandit (0 -> 0)
  Skips B404 (importing subprocess is not a defect) and B603, which fires on
  the recommended argv-list form and cannot judge whether input is trusted.
  shell=True and friends (B602/B604/B605/B609) stay enabled. Severity is
  deliberately not filtered, since real tests such as B105 report LOW.

Trivy (17 -> 1)
  `limit-severities-for-sarif` defaults to false, and unset it makes the action
  drop --severity for the SARIF run alone -- the table reported 0 findings
  while the uploaded SARIF carried all 17 at LOW/MEDIUM, every one below the
  declared CRITICAL,HIGH threshold. Now split in two: CVEs, secrets and
  licences at CRITICAL,HIGH with the limit applied; misconfiguration at every
  severity, because Trivy rates nearly every Kubernetes check LOW or MEDIUM and
  the same threshold would silence that scanner outright. Noise is controlled
  by scope instead -- vendored charts/litellm-helm accounted for 16 of the 17
  and is now skipped, matching the Checkov workflow.

  Kept: KSV-0106 in roles/llm_services/files/runtimes/vllm-runtime.yaml, the
  one finding in code this repo owns.

ansible-lint, Checkov
  No false positives; the 4 ansible-lint findings stand.

Verified locally against the pinned tool versions, under `bash -e` as Actions
runs it. actionlint clean.

@mramotowski mramotowski left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overall it looks good, some minor comments.

Comment thread .github/workflows/ansible-lint.yml Outdated
Comment on lines +53 to +56
# Style-only rule families are skipped: on these repos they produced 1158
# (ai-solutions) / 501 (inference) findings, of which ~5 were substantive.
# name[casing] alone fought the repo's deliberate `role | action | desc`
# convention 350 times. What is left is correctness and command-shell risk.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment needed? Why do we care about ai-solutions in enterprise-inference GH workflow?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, dropped. Justifying this repo's skip list by citing another repo's numbers was noise. The comment now carries only this repo's figures, re-measured: 499 findings unfiltered, of which 4 are substantive, with name[casing] accounting for 350 of them. Fixed in 810ec5e.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correcting the numbers I gave above: the baseline is 501 unfiltered / 6 substantive, not 499 / 4. name[casing] is unchanged at 350.

I had measured locally, where ansible-lint silently skips schema validation because it cannot fetch the JSON schemas, so two schema[meta] findings never appeared. They do in CI:

  2 schema profile:basic tags:core
  1 risky-shell-pipe profile:basic tags:command-shell
  3 no-changed-when profile:basic tags:command-shell,idempotency
Failed: 6 failure(s), 0 warning(s)

roles/langfuse/meta/main.yaml and roles/litellm/meta/main.yaml are the only role metas declaring galaxy_info, and the schema requires author whenever it is present. Both are genuine, and I have left them unfixed — consistent with every other real finding this PR reports rather than resolves.

ai-solutions is unaffected: schema is an unskippable rule, so its filtered CI run would have surfaced any schema finding, and it reported none. Comment corrected in 925010b.

pull_request:
types: [opened, synchronize, reopened, ready_for_review]
schedule:
# Weekly re-scan so new findings surface without a code change.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is true but it won't work on branches. Schedule is working only on default branch (main) so we will need to make this PR to main branch also to enable this trigger.

@Mushtaq-BGA Mushtaq-BGA Sep 3, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct, and thanks — that would have been a silent trap. I have put the caveat in the file itself rather than leaving it implicit, on all four scheduled workflows (trivy, bandit, checkov, scorecard):

  schedule:
    # Weekly re-scan so new findings surface without a code change.
    # NOTE: GitHub only fires `schedule` on the repository's default
    # branch, so this trigger stays dormant until these workflows are
    # also merged to `main`. The push and pull_request triggers above
    # work on every listed branch regardless.

So the weekly re-scan starts working once this lands on main; push and pull_request are unaffected on release-1.0 in the meantime.

Comment thread .github/workflows/bandit.yml Outdated
Comment on lines +73 to +75
bandit -r . -x ./venv,./.venv,./tests -s B101,B404,B601,B603 \
-f sarif -o bandit-results.sarif --exit-zero
bandit -r . -x ./venv,./.venv,./tests -s B101,B404,B601,B603 -f txt --exit-zero

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are running same cmd with different outputs twice. It doubles execution time. I know that it is short but still maybe we could do something like:

bandit -r . -x ./venv,./.venv,./tests -s B101,B404,B601,B603 \
  -f sarif -o bandit-results.sarif --exit-zero && \
cat bandit-results.sarif | python3 -m json.tool | grep -E '"(ruleId|message|level)"' || true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — the second invocation existed only to print something readable and re-scanned the whole tree to do it. Now a single scan, with the summary rendered from the SARIF that scan already wrote. I went with jq over grep on json.tool output so the rule, level and file:line stay associated per finding rather than becoming three separate streams of matches:

      - name: Run Bandit
        run: |
          bandit -r . -x ./venv,./.venv,./tests -s B101,B404,B601,B603 \
            -f sarif -o bandit-results.sarif --exit-zero
          jq -r '"Bandit: \(.runs[0].results | length) finding(s)",
            (.runs[0].results[]
              | "  \(.level)\t\(.ruleId)\t"
                + "\(.locations[0].physicalLocation.artifactLocation.uri)"
                + ":\(.locations[0].physicalLocation.region.startLine)\t"
                + "\(.message.text | split("\n")[0])")' \
            bandit-results.sarif

Output, from actually running the generated step:

Bandit: 1 finding(s)
  note	B607	roles/postgresql/files/provision_db.py:34	Starting a process with a partial executable path

It degrades to Bandit: 0 finding(s) with no jq error when there is nothing to report, which is the case on this repo. Fixed in 810ec5e.

Three points from @mramotowski on #6, applied to both
repos so the two stay identical.

Drop the cross-repo reference in the ansible-lint comment. Justifying a skip
list in one repo by citing another repo's finding count was noise; each file now
carries only its own numbers, measured fresh.

Note that `schedule` only fires from the default branch. Correct, and worth
saying in the file rather than leaving as a trap: the weekly crons stay dormant
until these workflows also land on `main`. The `push` and `pull_request`
triggers are unaffected. Added to all four scheduled workflows.

Scan once in Bandit instead of twice. The second invocation existed only to
print something human-readable, and re-scanned the whole tree to do it. The
summary is now rendered with `jq` from the SARIF the first scan already wrote,
which also keeps the two outputs from ever disagreeing:

    Bandit: 1 finding(s)
      note  B607  roles/postgresql/files/provision_db.py:34  Starting a process
                                                             with a partial
                                                             executable path

No change to what any scan reports. Verified by extracting the generated Bandit
step from the YAML and running it verbatim under `bash -e` in both repos --
1 finding in ai-solutions, 0 in inference, and the summary degrades cleanly to
"0 finding(s)". ansible-lint still reports 5 and 4. actionlint clean.
The counts in the skip-list rationale were measured locally, where
ansible-lint silently skips schema validation because it cannot fetch the
JSON schemas. Two schema[meta] findings therefore only appear in CI:
roles/langfuse and roles/litellm are the only role metas declaring
galaxy_info, and the schema requires 'author' whenever it is present.

Baseline is 501 findings unfiltered and 6 substantive, not 499 and 4.
name[casing] is unchanged at 350. ai-solutions is unaffected -- schema is
an unskippable rule, so CI's filtered run would have surfaced any schema
finding there, and it reported none.

The two schema[meta] findings are left unfixed, consistent with every
other genuine finding this PR reports rather than resolves.
Mushtaq-BGA added a commit to intel/enterprise-ai-solutions that referenced this pull request Sep 3, 2026
Three points from @mramotowski on intel/enterprise-inference#6, applied to both
repos so the two stay identical.

Drop the cross-repo reference in the ansible-lint comment. Justifying a skip
list in one repo by citing another repo's finding count was noise; each file now
carries only its own numbers, measured fresh.

Note that `schedule` only fires from the default branch. Correct, and worth
saying in the file rather than leaving as a trap: the weekly crons stay dormant
until these workflows also land on `main`. The `push` and `pull_request`
triggers are unaffected. Added to all four scheduled workflows.

Scan once in Bandit instead of twice. The second invocation existed only to
print something human-readable, and re-scanned the whole tree to do it. The
summary is now rendered with `jq` from the SARIF the first scan already wrote,
which also keeps the two outputs from ever disagreeing:

    Bandit: 1 finding(s)
      note  B607  roles/postgresql/files/provision_db.py:34  Starting a process
                                                             with a partial
                                                             executable path

No change to what any scan reports. Verified by extracting the generated Bandit
step from the YAML and running it verbatim under `bash -e` in both repos --
1 finding in ai-solutions, 0 in inference, and the summary degrades cleanly to
"0 finding(s)". ansible-lint still reports 5 and 4. actionlint clean.

@AhmedSeemalK AhmedSeemalK left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@Mushtaq-BGA
Mushtaq-BGA merged commit d51725c into release-1.0 Sep 3, 2026
8 checks passed
AhmedSeemalK added a commit to intel/enterprise-ai-solutions that referenced this pull request Sep 7, 2026
* Rename product to Intel® AI for Enterprise Solutions (#2)

* docs: rename product to Intel® AI Enterprise Solutions

* docs: clarify this release delivers the inference foundation for RAG/agentic services

* docs: update solutions architecture diagram

* docs: correct product name to Intel® AI for Enterprise Solutions

---------

Co-authored-by: release-bot <release-bot@users.noreply.github.com>

* docs: fix broken install command, dead anchor, and vague heading (#4)

* docs: fix broken install command path and dead traditional-ml anchor

* docs: fix dead traditional-ml anchor in deploy_models

* docs: rename vague '30-second integration' heading to describe the steps

---------

Co-authored-by: release-bot <release-bot@users.noreply.github.com>

* ci: add pinned security scanning workflows

Add SARIF-publishing security scans for the release branch, following the
pattern already used across the intel org (harden-runner + per-tool
workflow files + OpenSSF Scorecard):

- trivy       vulnerabilities, secrets, misconfig, licenses (filesystem)
- bandit      Python security findings
- checkov     Kubernetes / Actions / secrets IaC checks
- shellcheck  shell script correctness and quoting
- ansible-lint Ansible role and playbook hygiene
- scorecard   OpenSSF supply-chain posture rating

Every action is pinned to a full commit SHA and every tool to an exact
version, so a given commit always scans the same way and version bumps are
explicit, reviewable diffs. ShellCheck is fetched from its upstream release
and checksum-verified rather than installed via apt, whose version drifts
with the runner image.

Each scan was run locally against the tree before landing, and two were
retuned as a result:

- checkov drops the `helm` framework. Checkov renders charts into a random
  temp dir and reports paths relative to it, so all 50 (ai-solutions) / 40
  (inference) findings pointed at files that do not exist in the repo --
  unanchorable in the Security tab and re-reported as new every run. They
  were also entirely in vendored upstream charts. Trivy's misconfig scanner
  covers chart misconfiguration with correct paths.
- ansible-lint skips style-only rule families, which accounted for 1158 of
  1163 findings in ai-solutions and 496 of 502 in inference. name[casing]
  alone fired 350 times against the deliberate `role | action | desc`
  naming convention.

Jobs are soft-fail for the initial rollout: findings surface under
Security -> Code scanning instead of blocking the branch, so the existing
backlog can be triaged before any check is made required.

CodeQL is intentionally not added here - it already runs via code scanning
default setup on both repositories.

* ci: eliminate false positives from the security scans

Triage of every finding the scans produced, separating defects that cannot be
true from ones worth a maintainer's time. Every remaining finding was checked
by hand against the source and is real.

ShellCheck (20 -> 18)
  Discovery now reads shebangs as well as *.sh suffixes; the extension-less
  `model-manager` was previously never scanned. Entry points are linted with -x
  plus a --source-path list so cross-file globals resolve. This repo has no
  script that another script sources, so the per-variable SC2034 handling the
  shared workflow applies to sourced helper scripts is inert here.

  Two annotations, no behaviour change:
  - es_auto_installer.sh: the commas in `sudo --preserve-env=a,b,c` are sudo's
    own list syntax, not array separators (SC2054).
  - probe-node-topology.sh: `# shellcheck shell=sh`. The file has no shebang
    because it is read with lookup('file', ...) and piped into a pod's shell,
    never executed directly (SC2148). It is clean as POSIX sh.

  Kept: MODEL_NAMESPACES is genuinely unset-and-unused in
  calculate_actual_infrastructure_cpus.sh, and reads like a missing exclusion;
  11 SC2155 and 6 SC2207 are real.

Bandit (4 -> 1)
  Skips B404 (importing subprocess is not a defect) and B603, which fires on
  the recommended argv-list form and cannot judge whether input is trusted.
  shell=True and friends (B602/B604/B605/B609) stay enabled. Severity is
  deliberately not filtered, since real tests such as B105 report LOW.

  Kept: B607, `psql` resolved through PATH in provision_db.py.

Trivy (0 -> 0, plus a fixed threshold leak)
  `limit-severities-for-sarif` defaults to false, and unset it makes the action
  drop --severity for the SARIF run alone -- the table reported 0 findings
  while the uploaded SARIF carried LOW/MEDIUM ones the declared CRITICAL,HIGH
  threshold should have excluded. Now split in two: CVEs, secrets and licences
  at CRITICAL,HIGH with the limit applied; misconfiguration at every severity,
  because Trivy rates nearly every Kubernetes check LOW or MEDIUM and the same
  threshold would silence that scanner outright. Noise is controlled by scope
  instead -- vendored charts/ is skipped, matching the Checkov workflow.

ansible-lint, Checkov
  No false positives. The 5 ansible-lint findings stand: `kubectl get nodes |
  grep -v ' Ready' | wc -l` reports 0 not-ready nodes when kubectl itself
  fails, which is the bug pipefail prevents.

Verified locally against the pinned tool versions, under `bash -e` as Actions
runs it. actionlint clean.

* ci: address review feedback on the scanning workflows

Three points from @mramotowski on intel/enterprise-inference#6, applied to both
repos so the two stay identical.

Drop the cross-repo reference in the ansible-lint comment. Justifying a skip
list in one repo by citing another repo's finding count was noise; each file now
carries only its own numbers, measured fresh.

Note that `schedule` only fires from the default branch. Correct, and worth
saying in the file rather than leaving as a trap: the weekly crons stay dormant
until these workflows also land on `main`. The `push` and `pull_request`
triggers are unaffected. Added to all four scheduled workflows.

Scan once in Bandit instead of twice. The second invocation existed only to
print something human-readable, and re-scanned the whole tree to do it. The
summary is now rendered with `jq` from the SARIF the first scan already wrote,
which also keeps the two outputs from ever disagreeing:

    Bandit: 1 finding(s)
      note  B607  roles/postgresql/files/provision_db.py:34  Starting a process
                                                             with a partial
                                                             executable path

No change to what any scan reports. Verified by extracting the generated Bandit
step from the YAML and running it verbatim under `bash -e` in both repos --
1 finding in ai-solutions, 0 in inference, and the summary degrades cleanly to
"0 finding(s)". ansible-lint still reports 5 and 4. actionlint clean.

* fix: pin the enterprise-inference clone to v1.0.0

config/repos.yaml tracked `main` on the external inference repo, so the tree
the installer cloned changed underneath us with every upstream merge. Two
installs from the same solutions commit could produce different role trees,
which makes a deployment neither reproducible nor reviewable.

Pin to the v1.0.0 tag instead.

A bare commit SHA would be stronger, but `git clone --branch` accepts only
branch and tag names: a SHA fails the clone and ensure_repos' `||` fallback
then clones the default branch silently, which is strictly worse than tracking
main openly. Using the tag needs no installer change at all. The field docs now
state what the field accepts and call out the SHA trap.

Verified by running the installer's real ensure_repos() against this config
with es_auto_installer.sh unmodified: HEAD lands on the tagged commit,
detached, `describe --exact-match` reports v1.0.0, roles/ is found and
ANSIBLE_ROLES_PATH is correct.

* ci: raise ShellCheck threshold to error to suppress style warnings

---------

Co-authored-by: Mushtaq <mushtaq.ahmed@intel.com>
Co-authored-by: release-bot <release-bot@users.noreply.github.com>
Co-authored-by: sysadmin <sysadmin@localhost>
AhmedSeemalK added a commit that referenced this pull request Sep 7, 2026
* Merge pull request #2 from Mushtaq-BGA/release-1.0

Docs: rename to Intel® AI for Enterprise Inference + release doc fixes

* docs: brand component badge as Intel AI for Enterprise Solutions (#3)

Co-authored-by: release-bot <release-bot@users.noreply.github.com>

* docs: add Intel® trademark to component badge (#4)

Co-authored-by: release-bot <release-bot@users.noreply.github.com>

* ci: add pinned security scanning workflows

Add SARIF-publishing security scans for the release branch, following the
pattern already used across the intel org (harden-runner + per-tool
workflow files + OpenSSF Scorecard):

- trivy       vulnerabilities, secrets, misconfig, licenses (filesystem)
- bandit      Python security findings
- checkov     Kubernetes / Actions / secrets IaC checks
- shellcheck  shell script correctness and quoting
- ansible-lint Ansible role and playbook hygiene
- scorecard   OpenSSF supply-chain posture rating

Every action is pinned to a full commit SHA and every tool to an exact
version, so a given commit always scans the same way and version bumps are
explicit, reviewable diffs. ShellCheck is fetched from its upstream release
and checksum-verified rather than installed via apt, whose version drifts
with the runner image.

Each scan was run locally against the tree before landing, and two were
retuned as a result:

- checkov drops the `helm` framework. Checkov renders charts into a random
  temp dir and reports paths relative to it, so all 50 (ai-solutions) / 40
  (inference) findings pointed at files that do not exist in the repo --
  unanchorable in the Security tab and re-reported as new every run. They
  were also entirely in vendored upstream charts. Trivy's misconfig scanner
  covers chart misconfiguration with correct paths.
- ansible-lint skips style-only rule families, which accounted for 1158 of
  1163 findings in ai-solutions and 496 of 502 in inference. name[casing]
  alone fired 350 times against the deliberate `role | action | desc`
  naming convention.

Jobs are soft-fail for the initial rollout: findings surface under
Security -> Code scanning instead of blocking the branch, so the existing
backlog can be triaged before any check is made required.

CodeQL is intentionally not added here - it already runs via code scanning
default setup on both repositories.

* ci: eliminate false positives from the security scans

Triage of every finding the scans produced, separating defects that cannot be
true from ones worth a maintainer's time. Every remaining finding was checked by
hand against the source and is real.

ShellCheck (33 -> 7)
  Discovery now reads shebangs as well as *.sh suffixes: model_manager/model-
  manager is the largest script in the tree at 1429 lines and a *.sh glob never
  saw it.

  Entry points are linted with -x so the helper scripts they source are
  followed, plus a --source-path list -- the source statements are built from
  "$MM_LIB/..." which ShellCheck cannot resolve unaided, and without it 7
  globals that model_manager/lib/ does set and model-manager does read looked
  unused.

  The helper scripts under model_manager/lib/ are linted on their own.
  ShellCheck sees one file at a time, so a global a helper sets for its caller
  reads as unused: 26 of the 31 SC2034 hits were that. Disabling SC2034 for
  those files was too blunt -- it also hid four globals that genuinely are
  written and never read anywhere. Each hit is now checked individually and
  suppressed only when another file demonstrably reads that variable, limited to
  SHOUTY_CASE names of 3+ characters since a short lowercase name such as a
  loop's `i` collides textually with unrelated files.

  Kept, all verified by grepping the whole tree:
  - config.sh:266  RUNTIME_DEVICE, parsed from the runtime JSON beside five
    siblings that are all consumed, then never read -- cpu/gpu selection is
    read and dropped.
  - nri.sh:82,83   NRI_ADVANCED_MODE and NRI_HIDE_HT, both named in the
    function's own "Sets:" header comment as outputs, neither consumed. The
    Python side reads the MM_NRI_HIDE_HT environment variable, not this.
  - nri.sh:241     NRI_TARGET_NODE, set beside NRI_POLICY_NAME which is used.
  - helpers.sh:244 `local i` in a `for i in $(seq 1 24)` retry loop whose body
    never uses it.
  - helpers.sh:88, routing.sh:288  SC2155.

  No file under model_manager/ was modified.

Bandit (0 -> 0)
  Skips B404 (importing subprocess is not a defect) and B603, which fires on
  the recommended argv-list form and cannot judge whether input is trusted.
  shell=True and friends (B602/B604/B605/B609) stay enabled. Severity is
  deliberately not filtered, since real tests such as B105 report LOW.

Trivy (17 -> 1)
  `limit-severities-for-sarif` defaults to false, and unset it makes the action
  drop --severity for the SARIF run alone -- the table reported 0 findings
  while the uploaded SARIF carried all 17 at LOW/MEDIUM, every one below the
  declared CRITICAL,HIGH threshold. Now split in two: CVEs, secrets and
  licences at CRITICAL,HIGH with the limit applied; misconfiguration at every
  severity, because Trivy rates nearly every Kubernetes check LOW or MEDIUM and
  the same threshold would silence that scanner outright. Noise is controlled
  by scope instead -- vendored charts/litellm-helm accounted for 16 of the 17
  and is now skipped, matching the Checkov workflow.

  Kept: KSV-0106 in roles/llm_services/files/runtimes/vllm-runtime.yaml, the
  one finding in code this repo owns.

ansible-lint, Checkov
  No false positives; the 4 ansible-lint findings stand.

Verified locally against the pinned tool versions, under `bash -e` as Actions
runs it. actionlint clean.

* ci: address review feedback on the scanning workflows

Three points from @mramotowski on #6, applied to both
repos so the two stay identical.

Drop the cross-repo reference in the ansible-lint comment. Justifying a skip
list in one repo by citing another repo's finding count was noise; each file now
carries only its own numbers, measured fresh.

Note that `schedule` only fires from the default branch. Correct, and worth
saying in the file rather than leaving as a trap: the weekly crons stay dormant
until these workflows also land on `main`. The `push` and `pull_request`
triggers are unaffected. Added to all four scheduled workflows.

Scan once in Bandit instead of twice. The second invocation existed only to
print something human-readable, and re-scanned the whole tree to do it. The
summary is now rendered with `jq` from the SARIF the first scan already wrote,
which also keeps the two outputs from ever disagreeing:

    Bandit: 1 finding(s)
      note  B607  roles/postgresql/files/provision_db.py:34  Starting a process
                                                             with a partial
                                                             executable path

No change to what any scan reports. Verified by extracting the generated Bandit
step from the YAML and running it verbatim under `bash -e` in both repos --
1 finding in ai-solutions, 0 in inference, and the summary degrades cleanly to
"0 finding(s)". ansible-lint still reports 5 and 4. actionlint clean.

* ci: correct ansible-lint baseline counts in comment

The counts in the skip-list rationale were measured locally, where
ansible-lint silently skips schema validation because it cannot fetch the
JSON schemas. Two schema[meta] findings therefore only appear in CI:
roles/langfuse and roles/litellm are the only role metas declaring
galaxy_info, and the schema requires 'author' whenever it is present.

Baseline is 501 findings unfiltered and 6 substantive, not 499 and 4.
name[casing] is unchanged at 350. ai-solutions is unaffected -- schema is
an unskippable rule, so CI's filtered run would have surfaced any schema
finding there, and it reported none.

The two schema[meta] findings are left unfixed, consistent with every
other genuine finding this PR reports rather than resolves.

* ci: raise ShellCheck threshold to error to suppress style warnings

---------

Co-authored-by: Mushtaq <mushtaq.ahmed@intel.com>
Co-authored-by: release-bot <release-bot@users.noreply.github.com>
Co-authored-by: sysadmin <sysadmin@localhost>
kkurzacz-intel added a commit that referenced this pull request Sep 24, 2026
* Merge pull request #2 from Mushtaq-BGA/release-1.0

Docs: rename to Intel® AI for Enterprise Inference + release doc fixes

* docs: brand component badge as Intel AI for Enterprise Solutions (#3)

Co-authored-by: release-bot <release-bot@users.noreply.github.com>

* docs: add Intel® trademark to component badge (#4)

Co-authored-by: release-bot <release-bot@users.noreply.github.com>

* ci: add pinned security scanning workflows

Add SARIF-publishing security scans for the release branch, following the
pattern already used across the intel org (harden-runner + per-tool
workflow files + OpenSSF Scorecard):

- trivy       vulnerabilities, secrets, misconfig, licenses (filesystem)
- bandit      Python security findings
- checkov     Kubernetes / Actions / secrets IaC checks
- shellcheck  shell script correctness and quoting
- ansible-lint Ansible role and playbook hygiene
- scorecard   OpenSSF supply-chain posture rating

Every action is pinned to a full commit SHA and every tool to an exact
version, so a given commit always scans the same way and version bumps are
explicit, reviewable diffs. ShellCheck is fetched from its upstream release
and checksum-verified rather than installed via apt, whose version drifts
with the runner image.

Each scan was run locally against the tree before landing, and two were
retuned as a result:

- checkov drops the `helm` framework. Checkov renders charts into a random
  temp dir and reports paths relative to it, so all 50 (ai-solutions) / 40
  (inference) findings pointed at files that do not exist in the repo --
  unanchorable in the Security tab and re-reported as new every run. They
  were also entirely in vendored upstream charts. Trivy's misconfig scanner
  covers chart misconfiguration with correct paths.
- ansible-lint skips style-only rule families, which accounted for 1158 of
  1163 findings in ai-solutions and 496 of 502 in inference. name[casing]
  alone fired 350 times against the deliberate `role | action | desc`
  naming convention.

Jobs are soft-fail for the initial rollout: findings surface under
Security -> Code scanning instead of blocking the branch, so the existing
backlog can be triaged before any check is made required.

CodeQL is intentionally not added here - it already runs via code scanning
default setup on both repositories.

* ci: eliminate false positives from the security scans

Triage of every finding the scans produced, separating defects that cannot be
true from ones worth a maintainer's time. Every remaining finding was checked by
hand against the source and is real.

ShellCheck (33 -> 7)
  Discovery now reads shebangs as well as *.sh suffixes: model_manager/model-
  manager is the largest script in the tree at 1429 lines and a *.sh glob never
  saw it.

  Entry points are linted with -x so the helper scripts they source are
  followed, plus a --source-path list -- the source statements are built from
  "$MM_LIB/..." which ShellCheck cannot resolve unaided, and without it 7
  globals that model_manager/lib/ does set and model-manager does read looked
  unused.

  The helper scripts under model_manager/lib/ are linted on their own.
  ShellCheck sees one file at a time, so a global a helper sets for its caller
  reads as unused: 26 of the 31 SC2034 hits were that. Disabling SC2034 for
  those files was too blunt -- it also hid four globals that genuinely are
  written and never read anywhere. Each hit is now checked individually and
  suppressed only when another file demonstrably reads that variable, limited to
  SHOUTY_CASE names of 3+ characters since a short lowercase name such as a
  loop's `i` collides textually with unrelated files.

  Kept, all verified by grepping the whole tree:
  - config.sh:266  RUNTIME_DEVICE, parsed from the runtime JSON beside five
    siblings that are all consumed, then never read -- cpu/gpu selection is
    read and dropped.
  - nri.sh:82,83   NRI_ADVANCED_MODE and NRI_HIDE_HT, both named in the
    function's own "Sets:" header comment as outputs, neither consumed. The
    Python side reads the MM_NRI_HIDE_HT environment variable, not this.
  - nri.sh:241     NRI_TARGET_NODE, set beside NRI_POLICY_NAME which is used.
  - helpers.sh:244 `local i` in a `for i in $(seq 1 24)` retry loop whose body
    never uses it.
  - helpers.sh:88, routing.sh:288  SC2155.

  No file under model_manager/ was modified.

Bandit (0 -> 0)
  Skips B404 (importing subprocess is not a defect) and B603, which fires on
  the recommended argv-list form and cannot judge whether input is trusted.
  shell=True and friends (B602/B604/B605/B609) stay enabled. Severity is
  deliberately not filtered, since real tests such as B105 report LOW.

Trivy (17 -> 1)
  `limit-severities-for-sarif` defaults to false, and unset it makes the action
  drop --severity for the SARIF run alone -- the table reported 0 findings
  while the uploaded SARIF carried all 17 at LOW/MEDIUM, every one below the
  declared CRITICAL,HIGH threshold. Now split in two: CVEs, secrets and
  licences at CRITICAL,HIGH with the limit applied; misconfiguration at every
  severity, because Trivy rates nearly every Kubernetes check LOW or MEDIUM and
  the same threshold would silence that scanner outright. Noise is controlled
  by scope instead -- vendored charts/litellm-helm accounted for 16 of the 17
  and is now skipped, matching the Checkov workflow.

  Kept: KSV-0106 in roles/llm_services/files/runtimes/vllm-runtime.yaml, the
  one finding in code this repo owns.

ansible-lint, Checkov
  No false positives; the 4 ansible-lint findings stand.

Verified locally against the pinned tool versions, under `bash -e` as Actions
runs it. actionlint clean.

* ci: address review feedback on the scanning workflows

Three points from @mramotowski on #6, applied to both
repos so the two stay identical.

Drop the cross-repo reference in the ansible-lint comment. Justifying a skip
list in one repo by citing another repo's finding count was noise; each file now
carries only its own numbers, measured fresh.

Note that `schedule` only fires from the default branch. Correct, and worth
saying in the file rather than leaving as a trap: the weekly crons stay dormant
until these workflows also land on `main`. The `push` and `pull_request`
triggers are unaffected. Added to all four scheduled workflows.

Scan once in Bandit instead of twice. The second invocation existed only to
print something human-readable, and re-scanned the whole tree to do it. The
summary is now rendered with `jq` from the SARIF the first scan already wrote,
which also keeps the two outputs from ever disagreeing:

    Bandit: 1 finding(s)
      note  B607  roles/postgresql/files/provision_db.py:34  Starting a process
                                                             with a partial
                                                             executable path

No change to what any scan reports. Verified by extracting the generated Bandit
step from the YAML and running it verbatim under `bash -e` in both repos --
1 finding in ai-solutions, 0 in inference, and the summary degrades cleanly to
"0 finding(s)". ansible-lint still reports 5 and 4. actionlint clean.

* ci: correct ansible-lint baseline counts in comment

The counts in the skip-list rationale were measured locally, where
ansible-lint silently skips schema validation because it cannot fetch the
JSON schemas. Two schema[meta] findings therefore only appear in CI:
roles/langfuse and roles/litellm are the only role metas declaring
galaxy_info, and the schema requires 'author' whenever it is present.

Baseline is 501 findings unfiltered and 6 substantive, not 499 and 4.
name[casing] is unchanged at 350. ai-solutions is unaffected -- schema is
an unskippable rule, so CI's filtered run would have surfaced any schema
finding there, and it reported none.

The two schema[meta] findings are left unfixed, consistent with every
other genuine finding this PR reports rather than resolves.

* ci: raise ShellCheck threshold to error to suppress style warnings

* Introduce changes for patch v1.0.1 (#8)

* Release v1.0.1

Patch release on top of v1.0.0.

- model-manager: deduplicate args by flag name in LLMInferenceService
- model-manager: add --label flag for deployments
- model-manager: undeploy --wait blocks until pods terminate
- nfd: label namespace with PSS profile when enforce_pss is true
- llm_services: provision the model-store PVC from the resolved storage backend

Co-authored-by: jpiaseck <jakub.piasecki@intel.com>
Co-authored-by: Ant Albersk <anna.alberska@intel.com>
Co-authored-by: n-owi <mikolaj.nowicki@intel.com>
Co-authored-by: sgurunat <gurunath.s@intel.com>

* ci: match all release branches in workflow triggers

Trigger the security-scan workflows on release-[0-9]+.[0-9]+ branches
(e.g. release-1.0.1), not just release-1.0, and add the same branch
filter to pull_request events.

Co-authored-by: Maciej Ramotowski <maciej.ramotowski@intel.com>

---------

Co-authored-by: jpiaseck <jakub.piasecki@intel.com>
Co-authored-by: Ant Albersk <anna.alberska@intel.com>
Co-authored-by: n-owi <mikolaj.nowicki@intel.com>
Co-authored-by: sgurunat <gurunath.s@intel.com>
Co-authored-by: Maciej Ramotowski <maciej.ramotowski@intel.com>

---------

Co-authored-by: Mushtaq <mushtaq.ahmed@intel.com>
Co-authored-by: release-bot <release-bot@users.noreply.github.com>
Co-authored-by: sysadmin <sysadmin@localhost>
Co-authored-by: jpiaseck <jakub.piasecki@intel.com>
Co-authored-by: Ant Albersk <anna.alberska@intel.com>
Co-authored-by: n-owi <mikolaj.nowicki@intel.com>
Co-authored-by: sgurunat <gurunath.s@intel.com>
Co-authored-by: Maciej Ramotowski <maciej.ramotowski@intel.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants