Skip to content

Commit b1500f0

Browse files
jbujulaCopilot
andauthored
chore: automate dependency security fixes (#1441)
* chore: automate dependency security fixes Dependabot cannot read or update the npm `overrides` block, and almost every vulnerability in this repo is transitive and pinned through `overrides`. That gap is why the same S360/Component Governance alerts are re-raised every week and have to be fixed by hand. Adds: - scripts/audit-overrides.js - resolves `npm audit` output against the GitHub Advisory DB, compares each advisory against every installed copy in the lock file, and raises the `overrides` range only when the patched version is actually published. Reports (rather than guesses at) bundled copies, packages spanning several major lines, and advisories with no published fix. - (workflow file .github/workflows/dependency-security.yml is added in a follow-up; the push token lacks the `workflow` OAuth scope) - dependabot.yml - grouped minor/patch updates, github-actions ecosystem, and ignores for the deliberately pinned `ajv` and `azure-pipelines-task-lib`. Key finding driving the design: most alerts are a stale lock file rather than a too-low range. `postcss` at `^8.5.15` already permitted the patched `8.5.25`, and the same was true for `tar`, `ip-address` and `undici` - `npm update` alone fixes those, with no manifest edit. Validated against the unfixed tree on main: the script independently reproduced the manual fix plan (stale-lock refresh for tar/postcss/ip-address/undici, a real bump for adm-zip) and correctly refused to emit a flat `brace-expansion` override, which would have downgraded the 2.x and 5.x copies to 1.1.17. Also records both learnings in the security-alerts skill. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6ac571cd-d84e-4b40-805a-e316a62ed292 * chore: request review on dependency PRs via CODEOWNERS Dependabot's eviewers: option was retired in 2025 in favour of CODEOWNERS, and this repo had no CODEOWNERS file, so dependency PRs were raised with no reviewer requested and no notification to anyone. Scoped to the dependency and security-automation files only, so unrelated PRs are not affected. Nothing auto-merges - every PR still needs a human to merge it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6ac571cd-d84e-4b40-805a-e316a62ed292 * chore: assign dependency PR reviews to managed-app-devops-team Verified the team has \maintain\ (push) on this repo; CODEOWNERS silently ignores teams without write access. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6ac571cd-d84e-4b40-805a-e316a62ed292 * chore: add weekly dependency security refresh workflow Runs npm ci -> npm update -> scripts/audit-overrides.js --write -> npm install -> npm run ci, and opens a PR only if the build is green. Reviewers come from .github/CODEOWNERS. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6ac571cd-d84e-4b40-805a-e316a62ed292 * chore: distinguish feed-mirror lag from a genuinely missing fix The 1ES feed mirror can trail the public npm registry by days. brace-expansion 1.1.18 / 2.1.4 / 5.0.9 were published upstream and picked up by Dependabot (#1437) while \ pm view\ against our mirror still reported 5.0.8 as latest, which made an available fix look like an unfixable advisory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6ac571cd-d84e-4b40-805a-e316a62ed292 * chore: have CODEOWNERS cover every workflow file dependabot.yml enables the github-actions ecosystem, which raises PRs against any workflow (PullRequest.yml, codeql.yml, post-release.yml). CODEOWNERS only listed dependency-security.yml, so those PRs would have landed with no reviewer requested. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6ac571cd-d84e-4b40-805a-e316a62ed292 * fix: escape backslashes when quoting Windows command arguments CodeQL js/incomplete-sanitization: escaping only the quote character left a trailing backslash able to escape the closing quote. Backslash runs that precede a quote (or end the argument) are now doubled per Windows command-line parsing rules. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6ac571cd-d84e-4b40-805a-e316a62ed292 --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 6ac571cd-d84e-4b40-805a-e316a62ed292
1 parent ee560c3 commit b1500f0

5 files changed

Lines changed: 518 additions & 0 deletions

File tree

.claude/skills/security-alerts/SKILL.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,44 @@ If auth fails: run `gh auth login --hostname github.com --git-protocol https --w
9090
npm audit --json 2>&1
9191
```
9292

93+
### 1d — Map S360 CVE/GHSA ids to packages
94+
95+
Most `[S360]` items carry only a Component Governance `alertId` and no package name, and the
96+
Component Governance REST API is not reachable (`_apis/governance/...` returns 404). Resolve them
97+
against the GitHub Advisory DB instead — this reliably yields the package, the vulnerable range,
98+
and the real patched version:
99+
100+
```bash
101+
# From a CVE id (as it appears in the S360 work item title)
102+
gh api "/advisories?cve_id=CVE-2026-14257" \
103+
--jq '.[0] | "\(.ghsa_id) | \(.severity) | " + ([.vulnerabilities[]? | "\(.package.name) vuln=\(.vulnerable_version_range) patched=\(.first_patched_version)"] | join(" ;; "))'
104+
105+
# From a GHSA id
106+
gh api /advisories/GHSA-5c6j-r48x-rmvq \
107+
--jq '"\(.severity) | " + ([.vulnerabilities[]? | "\(.package.name) patched=\(.first_patched_version)"] | join(" ;; "))'
108+
```
109+
110+
Do this for every unmapped S360 item before declaring anything unresolvable.
111+
112+
---
113+
114+
## Step 1e — Run the resolver script first
115+
116+
`scripts/audit-overrides.js` already does steps 2 and 3 mechanically: it reads `npm audit`,
117+
resolves every advisory against the Advisory DB, compares against each installed copy in the
118+
lock file, and reports exactly which ranges need to move.
119+
120+
```bash
121+
node scripts/audit-overrides.js # report only
122+
node scripts/audit-overrides.js --write # apply the safe override bumps
123+
```
124+
125+
Use its output as the fix plan, then hand-fix only what it lists under **Needs a human**.
126+
127+
**Check the existing range before editing `package.json`.** Most alerts here are a stale lock
128+
file, not a too-low range — e.g. `postcss` at `^8.5.15` already permits the patched `8.5.25`.
129+
For those, `npm update` is the entire fix and `package.json` should not be touched.
130+
93131
---
94132

95133
## Step 2 — Build fix plan
@@ -114,6 +152,13 @@ Decision rules per vulnerability:
114152

115153
Known permanent accepted risk — skip: `elliptic` (GHSA-848j-6mx2-7j84) — dev-only, no patched version.
116154

155+
**"No published fix" may just be feed lag.** This machine resolves npm through the 1ES feed
156+
mirror (`ms-feed-*.pkgs.visualstudio.com`), which can trail the public registry by days. Before
157+
recording something as an accepted risk, cross-check against Dependabot — if Dependabot has
158+
opened a PR for a version `npm view` says does not exist, the fix is real and the mirror is
159+
simply behind. Say "not yet on our feed mirror", not "no fix exists", and let the Dependabot PR
160+
land it.
161+
117162
---
118163

119164
## Step 3 — Apply fixes
@@ -132,6 +177,9 @@ Hard rules:
132177
- Never add `"minimatch": "^3.x"` flat override — infinite npm loop
133178
- `ajv` stays at `^6.x` — v8 breaks ESLint
134179
- Do not change: `nanoid`, `electron-to-chromium`, `@types/node` overrides
180+
- **Never add a flat override for a package installed across several major lines** — it
181+
rewrites every copy and silently downgrades the other lines. `brace-expansion` is installed
182+
on the 1.x, 2.x and 5.x lines simultaneously; each needs its own nested override.
135183

136184
### Strategy B — Direct dependency bump
137185

.github/CODEOWNERS

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# https://docs.github.com/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
2+
#
3+
# Dependabot's `reviewers:` option was retired in favour of CODEOWNERS, so this file is now
4+
# the only way to get a review automatically requested on dependency PRs.
5+
#
6+
# Scoped deliberately to the dependency and security-automation surface rather than the whole
7+
# repo, so it does not add a reviewer to every unrelated PR.
8+
#
9+
# @microsoft/managed-app-devops-team has `maintain` on this repo, which satisfies the write
10+
# access CODEOWNERS requires - entries naming a team without write access are silently ignored.
11+
12+
/package.json @microsoft/managed-app-devops-team
13+
/package-lock.json @microsoft/managed-app-devops-team
14+
/.github/dependabot.yml @microsoft/managed-app-devops-team
15+
/scripts/audit-overrides.js @microsoft/managed-app-devops-team
16+
17+
# All workflows, not just dependency-security.yml: the `github-actions` ecosystem in
18+
# dependabot.yml raises PRs against any workflow file (PullRequest.yml, codeql.yml,
19+
# post-release.yml), and those would otherwise land with no reviewer requested.
20+
/.github/workflows/ @microsoft/managed-app-devops-team

.github/dependabot.yml

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,45 @@
11
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
2+
#
3+
# Note: Dependabot only understands `dependencies` / `devDependencies`. It cannot read or
4+
# update the npm `overrides` block, which is where most transitive fixes in this repo live.
5+
# Those are handled by .github/workflows/dependency-security.yml instead.
6+
#
7+
# Reviewers are assigned via .github/CODEOWNERS - the `reviewers:` option below was retired
8+
# by GitHub in 2025 and no longer has any effect.
9+
#
10+
# Nothing here auto-merges. Every PR needs a human to review and merge it.
211

312
version: 2
413
updates:
514
- package-ecosystem: "npm"
615
directory: "/" # Location of package manifests
716
schedule:
817
interval: "weekly"
18+
open-pull-requests-limit: 10
19+
labels:
20+
- "dependencies"
21+
# Keep routine version churn to a single PR so security PRs stay easy to spot.
22+
groups:
23+
dev-dependencies:
24+
dependency-type: "development"
25+
update-types:
26+
- "minor"
27+
- "patch"
28+
production-dependencies:
29+
dependency-type: "production"
30+
update-types:
31+
- "minor"
32+
- "patch"
33+
ignore:
34+
# ajv v8 breaks ESLint 8 - see the overrides block in package.json.
35+
- dependency-name: "ajv"
36+
versions: ["8.x", ">=8"]
37+
# Pinned to unblock the official build.
38+
- dependency-name: "azure-pipelines-task-lib"
39+
40+
- package-ecosystem: "github-actions"
41+
directory: "/"
42+
schedule:
43+
interval: "weekly"
44+
labels:
45+
- "dependencies"
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# Copyright (c) Microsoft Corporation.
2+
# Licensed under the MIT License.
3+
#
4+
# Weekly dependency security refresh.
5+
#
6+
# Dependabot cannot see npm `overrides`, and almost every vulnerability in this repo is
7+
# transitive and pinned through `overrides`. That gap is why S360/Component Governance
8+
# keeps re-raising the same alerts and why they have to be fixed by hand each time.
9+
#
10+
# This workflow closes it:
11+
# 1. `npm update` refreshes package-lock.json inside the ranges already declared.
12+
# Historically this alone clears most alerts - the ranges were fine, the lock was stale.
13+
# 2. `scripts/audit-overrides.js --write` raises any range that is genuinely too low,
14+
# resolving each advisory against the GitHub Advisory DB for its real patched version.
15+
# 3. The build must pass before a PR is opened.
16+
#
17+
# Anything the script cannot fix safely (bundled copies, multi-major packages, advisories
18+
# with no published fix) is reported in the PR body for a human to pick up.
19+
name: Dependency security refresh
20+
21+
on:
22+
schedule:
23+
- cron: "0 6 * * 1" # Mondays 06:00 UTC
24+
workflow_dispatch:
25+
26+
permissions:
27+
contents: write
28+
pull-requests: write
29+
30+
jobs:
31+
refresh:
32+
runs-on: ubuntu-latest
33+
steps:
34+
- uses: actions/checkout@v4
35+
36+
- name: Setup Node.js environment
37+
uses: actions/setup-node@v4
38+
with:
39+
node-version: 20
40+
registry-url: https://npm.pkg.github.com
41+
42+
- name: Configure npm
43+
run: npm config set //npm.pkg.github.com/:_authToken=${{ secrets.GPR_ACCESS_TOKEN }}
44+
45+
- name: Install npm@11 globally
46+
run: npm i -g npm@11
47+
48+
- name: Install dependencies
49+
run: npm ci
50+
51+
- name: Refresh lock file within existing ranges
52+
run: npm update
53+
54+
- name: Raise ranges that are still too low
55+
id: overrides
56+
env:
57+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58+
run: |
59+
node scripts/audit-overrides.js --write | tee audit-report.md
60+
npm install
61+
62+
- name: Check for changes
63+
id: diff
64+
run: |
65+
if git diff --quiet -- package.json package-lock.json; then
66+
echo "changed=false" >> "$GITHUB_OUTPUT"
67+
else
68+
echo "changed=true" >> "$GITHUB_OUTPUT"
69+
fi
70+
71+
# The PR is only opened if the build is green, so a broken tree is never proposed.
72+
- name: Build and test
73+
if: steps.diff.outputs.changed == 'true'
74+
run: npm run ci
75+
env:
76+
AZ_DevOps_Read_PAT: ${{ secrets.AZ_DevOps_Read_PAT }}
77+
PA_BT_ORG_PASSWORD: ${{ secrets.PA_BT_ORG_PASSWORD }}
78+
RUN_ID: ${{ github.run_id }}
79+
80+
- name: Open pull request
81+
if: steps.diff.outputs.changed == 'true'
82+
env:
83+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
84+
run: |
85+
BRANCH="bot/dependency-refresh-$(date -u +%Y%m%d)"
86+
git config user.name "github-actions[bot]"
87+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
88+
git checkout -b "$BRANCH"
89+
git add package.json package-lock.json
90+
git commit -m "chore: weekly dependency security refresh"
91+
git push -u origin "$BRANCH"
92+
93+
{
94+
echo "## Summary"
95+
echo
96+
echo "Automated weekly refresh of vulnerable dependencies."
97+
echo
98+
cat audit-report.md
99+
echo
100+
echo "## Test plan"
101+
echo
102+
echo "- [x] \`npm ci\` clean"
103+
echo "- [x] \`npm run ci\` passed before this PR was opened"
104+
echo "- [ ] Functional tests - exempt (require \`PA_BT_ORG_PASSWORD\`)"
105+
echo
106+
echo "Anything listed under **Needs a human** could not be fixed safely by the script"
107+
echo "and still requires a manual override, a lock-file patch, or an accepted-risk note."
108+
} > pr-body.md
109+
110+
# No --assignee / --reviewer here: .github/CODEOWNERS already requests a review from
111+
# @microsoft/managed-app-devops-team for every file this PR touches, and passing a
112+
# team reviewer explicitly can fail depending on the GITHUB_TOKEN's permissions.
113+
gh pr create --base main --head "$BRANCH" \
114+
--title "chore: weekly dependency security refresh" \
115+
--body-file pr-body.md

0 commit comments

Comments
 (0)