diff --git a/.github/workflows/msdo-breach-monitor.lock.yml b/.github/workflows/msdo-breach-monitor.lock.yml index f1590815..945000f5 100644 --- a/.github/workflows/msdo-breach-monitor.lock.yml +++ b/.github/workflows/msdo-breach-monitor.lock.yml @@ -21,15 +21,12 @@ # For more information: https://github.github.com/gh-aw/introduction/overview/ # # -# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"148a5936b737a9676ee587cb8bd30999bfe4eae2d76dcfda6a8a6bddbe501b9b","compiler_version":"v0.61.0","strict":true} +# gh-aw-metadata: {"schema_version":"v2","frontmatter_hash":"8aff8c918da79899626a7f1870cfdc2c94bba2f747ff53f3abfd9892ab61aaf7","compiler_version":"v0.61.0","strict":true} name: "MSDO Toolchain Breach Monitor" "on": # roles: # Roles processed as role check in pre-activation job # - write # Roles processed as role check in pre-activation job - schedule: - - cron: "2 11 * * *" - # Friendly format: daily (scattered) workflow_dispatch: permissions: {} @@ -232,8 +229,6 @@ jobs: permissions: contents: read issues: read - concurrency: - group: "gh-aw-copilot-${{ github.workflow }}" env: DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} GH_AW_ASSETS_ALLOWED_EXTS: "" diff --git a/.github/workflows/msdo-breach-monitor.md b/.github/workflows/msdo-breach-monitor.md index eb03fdcd..61c89a05 100644 --- a/.github/workflows/msdo-breach-monitor.md +++ b/.github/workflows/msdo-breach-monitor.md @@ -75,9 +75,9 @@ Monitor for supply chain security incidents affecting any tool in the MSDO toolc The `toolchain-version-probe` workflow runs weekly, installs every tool through the real MSDO CLI, and records exactly which package version was resolved into `.github/toolchain-versions.json`. These are the versions MSDO users actually download — not registry "latest", but the version pinned in MSDO's `.gdntool` configs. -**Read the file from this repository:** +**Read the file from this repository (the probe pushes to a dedicated branch to avoid branch protection on main):** ``` -GET https://api.github.com/repos/microsoft/security-devops-action/contents/.github/toolchain-versions.json +GET https://api.github.com/repos/microsoft/security-devops-action/contents/.github/toolchain-versions.json?ref=bot/toolchain-versions ``` Decode the base64 `content` field. The `tools` object maps each tool name to its resolved version. The `generated_at` field tells you when the probe last ran. diff --git a/.github/workflows/toolchain-version-probe.yml b/.github/workflows/toolchain-version-probe.yml index 7ee90fa0..a87751b2 100644 --- a/.github/workflows/toolchain-version-probe.yml +++ b/.github/workflows/toolchain-version-probe.yml @@ -3,6 +3,12 @@ name: MSDO Toolchain Version Probe # Runs MSDO to install tools as a side effect, then scrapes the install # directories to record exact resolved versions into toolchain-versions.json. # The breach monitor reads this file instead of guessing "latest" from registries. +# +# Guardian installs all tool wrappers as NuGet packages into: +# /home/runner/work/_msdo/packages/nuget/{PackageName}.{version}/ +# ESLint is installed via npm into: +# /home/runner/work/_msdo/packages/node_modules/eslint/ +# Package names confirmed from run 23433052319. on: schedule: @@ -20,11 +26,9 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 - # Run MSDO so it downloads and installs all tool binaries into .gdn/i/. - # Scan may find nothing (no real targets) — that is fine. We only care - # about the side effect: tool packages installed in .gdn/i/{type}/. + # Run MSDO — scan may find nothing (no real targets), that's fine. + # Side effect: Guardian downloads all tool packages into _msdo/packages/nuget/. - name: Install MSDO tools - id: msdo uses: microsoft/security-devops-action@main continue-on-error: true with: @@ -35,73 +39,71 @@ jobs: python3 - <<'PYEOF' import os, json, re, pathlib, datetime - # MSDO installs packages into .gdn/i/{type}/{PackageName}.{version}/ - # DotNetToolClient.cs:244 → nuget dirs are PackageName.Version - # ZipClient / Pip3Client → same pattern via PackageInstaller - GDN_I = pathlib.Path('.gdn/i') + NUGET_DIR = pathlib.Path('/home/runner/work/_msdo/packages/nuget') + NPM_DIR = pathlib.Path('/home/runner/work/_msdo/packages/node_modules') VER_PAT = re.compile(r'^(.+?)\.(v?\d+\.\d+(?:\.\d+)*(?:[-+][0-9A-Za-z.-]+)?)$', re.IGNORECASE) - # Map installed package names → canonical tool names used in our inventory. - # Keys are lowercase. Add new entries after the first probe run reveals - # the exact package names for terrascan, trivy, eslint, bandit, checkov. + # Guardian NuGet wrapper package names → canonical tool names. + # Confirmed from run 23433052319 (_msdo/packages/nuget/ directory listing). PKG_TO_TOOL = { - # NuGet - 'microsoft.codeanalysis.binskim': 'binskim', - 'microsoft.azure.templates.analyzer': 'templateanalyzer', - # pip (package name == tool name for these) - 'bandit': 'bandit', - 'checkov': 'checkov', - # npm - 'eslint': 'eslint', - # zip / GitHub releases (names TBD from first run — check raw_dirs) - 'trivy': 'trivy', - 'terrascan': 'terrascan', + 'microsoft.guardian.banditredist_linux_amd64': 'bandit', + 'microsoft.codeanalysis.binskim': 'binskim', + 'microsoft.guardian.checkovredist_linux_amd64': 'checkov', + 'azure.templates.analyzer.commandline.linux-x64': 'templateanalyzer', + 'microsoft.guardian.terrascanredist_linux_amd64': 'terrascan', + 'microsoft.guardian.trivyredist_linux_amd64': 'trivy', } - # Internal CLI package — skip in output - CLI_PKGS = { + # Internal packages — skip + SKIP_PKGS = { 'microsoft.security.devops.cli', 'microsoft.security.devops.cli.linux-x64', 'microsoft.security.devops.cli.linux-arm64', 'microsoft.security.devops.cli.win-x64', + 'microsoft.security.devops.policy.names', + 'microsoft.security.devops.policy.github', } tools = {} - raw_dirs = {} - - for pkg_type in ('nuget', 'pip', 'npm', 'zip'): - type_dir = GDN_I / pkg_type - if not type_dir.exists(): - continue - entries = sorted(d.name for d in type_dir.iterdir() if d.is_dir()) - raw_dirs[pkg_type] = entries + raw_dirs = [] + + if NUGET_DIR.exists(): + entries = sorted(d.name for d in NUGET_DIR.iterdir() if d.is_dir()) + raw_dirs = entries for name in entries: m = VER_PAT.match(name) if not m: continue pkg_lower = m.group(1).lower() version = m.group(2) - if pkg_lower in CLI_PKGS: + if pkg_lower in SKIP_PKGS: continue canonical = PKG_TO_TOOL.get(pkg_lower) - if canonical is None: - continue - tools[canonical] = version + if canonical: + tools[canonical] = version - output = { - 'generated_at': datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'), - 'msdo_cli_version': os.environ.get('MSDO_INSTALLEDVERSION', 'unknown'), - 'tools': tools, - 'raw_dirs': raw_dirs, - } + # ESLint: installed via npm, read version from package.json + eslint_pkg = NPM_DIR / 'eslint' / 'package.json' + if eslint_pkg.exists(): + tools['eslint'] = json.loads(eslint_pkg.read_text())['version'] + + print('raw_dirs:', raw_dirs) + print('resolved:', tools) - expected = set(PKG_TO_TOOL.values()) - missing = expected - set(tools.keys()) if not tools: - raise SystemExit('ERROR: no tool versions resolved — .gdn/i/ may be empty. Aborting to avoid poisoning toolchain-versions.json.') + raise SystemExit('ERROR: no versions resolved — _msdo/packages/nuget/ empty or missing. Aborting.') + + missing = (set(PKG_TO_TOOL.values()) | {'eslint'}) - set(tools.keys()) if missing: - print(f'WARNING: expected tools not found in install dirs: {sorted(missing)}') + print(f'WARNING: expected tools not found: {sorted(missing)}') + + output = { + 'generated_at': datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'), + 'msdo_cli_version': os.environ.get('MSDO_INSTALLEDVERSION', 'unknown'), + 'tools': tools, + 'raw_dirs': raw_dirs, + } out = pathlib.Path('.github/toolchain-versions.json') out.parent.mkdir(parents=True, exist_ok=True) @@ -118,5 +120,7 @@ jobs: echo "toolchain-versions.json unchanged — nothing to commit" else git commit -m "chore(ci): update toolchain-versions.json [skip ci]" - git push + # Push to dedicated unprotected branch — main has branch protection + # requiring PRs. The breach monitor reads from this branch via API. + git push origin HEAD:bot/toolchain-versions --force fi