Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# CODEOWNERS — automatic review routing for the FastTrack catalog.
#
# Order matters: the LAST matching pattern wins. Replace the placeholder team
# handles below with the real maintainer team(s) before relying on required
# reviews (Settings ▸ Branches ▸ "Require review from Code Owners").
#
# NOTE: CODEOWNERS entries must reference users/teams that have write access to
# this repository, otherwise GitHub silently ignores them.

# Default owners for everything in the repo.
* @microsoft/fasttrack-maintainers

# --- Security-sensitive / high blast-radius paths -------------------------
# CI, automation, supply chain, licensing and the generated catalog must be
# reviewed by maintainers regardless of who else owns the surrounding content.
/.github/ @microsoft/fasttrack-maintainers
/.github/workflows/ @microsoft/fasttrack-maintainers
/.github/scripts/ @microsoft/fasttrack-maintainers
/.github/pr-sweeper.config.mjs @microsoft/fasttrack-maintainers
/.github/dependabot.yml @microsoft/fasttrack-maintainers
/tools/catalog-build/ @microsoft/fasttrack-maintainers
/catalog.json @microsoft/fasttrack-maintainers
/index.html @microsoft/fasttrack-maintainers
/SECURITY.MD @microsoft/fasttrack-maintainers
/LICENSE @microsoft/fasttrack-maintainers
/LICENSE-CODE @microsoft/fasttrack-maintainers
/CONTRIBUTING.md @microsoft/fasttrack-maintainers
32 changes: 32 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# Dependabot keeps the automation supply chain patched.
# Docs: https://docs.github.com/code-security/dependabot/dependabot-version-updates
version: 2
updates:
# GitHub Actions used by every workflow (pinned to SHAs — Dependabot still
# tracks the upstream tags behind those SHAs and proposes bumps).
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
open-pull-requests-limit: 5
commit-message:
prefix: "chore(actions)"
labels:
- "dependencies"
- "github-actions"

# The catalog build tooling (Node / npm).
- package-ecosystem: "npm"
directory: "/tools/catalog-build"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
open-pull-requests-limit: 5
commit-message:
prefix: "chore(deps)"
labels:
- "dependencies"
- "javascript"
145 changes: 145 additions & 0 deletions .github/pr-sweeper.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
// Intelligent PR Sweeper — configuration
// ---------------------------------------
// This file is ALWAYS loaded from the trusted base repository (never from a
// pull-request head), so a contributor cannot weaken the guardrails by editing
// their own copy. Tune the values here; no workflow YAML edits required.
//
// Everything is plain JavaScript, so you can add comments and computed values.

export default {
// Where catalog resources are allowed to live. Changes entirely outside these
// roots (plus the always-allowed housekeeping paths below) get flagged as
// "out of scope" so reviewers notice unexpected edits.
contentRoots: [
"scripts",
"samples",
"tools",
"docs",
"copilot-agent-samples",
"copilot-agent-strategy",
"copilot-analytics-samples",
"copilot-prompt-samples",
"traffic-data",
],

// Paths a normal contribution may touch even though they are not content roots.
alwaysAllowedPaths: [
"README.md",
"CHANGELOG.md",
"TEMPLATE-README.md",
".gitignore",
],

// Editing anything matching these globs is not blocked, but it is surfaced
// prominently and always routed to a maintainer / security review. These are
// the "blast radius" files: CI, supply chain, licensing, generated catalog.
sensitivePaths: [
".github/workflows/**",
".github/scripts/**",
".github/pr-sweeper.config.mjs",
".github/CODEOWNERS",
".github/dependabot.yml",
"catalog.json", // generated — must never be hand-edited
"LICENSE",
"LICENSE-CODE",
"SECURITY.MD",
],

files: {
// Added files with these extensions are blocked. Binaries and archives are a
// common vector for slipping unreviewable / malicious content into a repo.
blockedExtensions: [
".exe", ".dll", ".so", ".dylib", ".bin", ".msi", ".bat", ".cmd", ".com",
".scr", ".jar", ".zip", ".7z", ".rar", ".gz", ".tar", ".tgz", ".iso",
".pfx", ".p12", ".keystore", ".jks",
],
// Binary formats we legitimately publish (Power BI, PowerPoint, images).
// These are allowed but still size-checked.
allowedBinaryExtensions: [".pbix", ".pptx", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".ico", ".pdf"],
// Any added/binary file larger than this is flagged for manual review.
maxFileSizeBytes: 5 * 1024 * 1024, // 5 MB
// A single PR touching more than this many files is flagged as "large"
// (contributor-friendliness: we ask them to split, we do not block).
largeChangeFileCount: 60,
},

scope: {
// How many distinct content roots a single PR may touch before we nudge the
// contributor to split it (CONTRIBUTING asks for one focused change).
maxContentRoots: 3,
},

review: {
// Dual-model review. Each slot is an independent reviewer; their findings are
// shown side by side plus a merged consensus. AI output is ADVISORY ONLY and
// never gates a merge — the deterministic guardrails are authoritative.
//
// provider "github-models" needs only `permissions: models: read` (built-in
// GITHUB_TOKEN). To use a model GitHub Models does not host (e.g. Anthropic
// Claude Opus 4.8), set provider "azure-openai" and supply the endpoint here
// plus the api key via the AZURE_OPENAI_API_KEY repo secret.
providers: [
{
slot: "fast",
label: "GPT-class reviewer",
provider: "github-models",
model: "openai/gpt-5",
temperature: 0.1,
},
{
slot: "deep",
label: "Deep-reasoning reviewer",
provider: "github-models",
model: "openai/o3",
// o-series reasoning models reject a non-default temperature, so we omit
// it (null = "don't send the field"). Set a number for non-reasoning models.
temperature: null,
// To wire the *real* Claude Opus 4.8 here instead, deploy it on
// Azure AI Foundry (Anthropic models are available there) and use:
// provider: "azure-openai",
// endpoint: "https://<resource>.services.ai.azure.com/models",
// model: "<your-opus-4.8-deployment>",
// then add the AZURE_OPENAI_API_KEY secret. Leave as-is to run on the
// built-in, zero-secret GitHub Models path.
},
],
maxDiffChars: 60000, // truncate very large diffs sent to the models
timeoutMs: 60000,
},

// Deterministic secret detectors. Matched only against ADDED lines in the diff.
// Keep evidence redacted in output — never echo a full secret back into a PR.
secretPatterns: [
{ id: "private-key", label: "Private key block", regex: "-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----", severity: "block" },
{ id: "aws-access-key", label: "AWS access key id", regex: "\\bAKIA[0-9A-Z]{16}\\b", severity: "block" },
{ id: "github-pat", label: "GitHub token", regex: "\\bgh[pousr]_[A-Za-z0-9]{36,}\\b", severity: "block" },
{ id: "github-fine-pat", label: "GitHub fine-grained token", regex: "\\bgithub_pat_[A-Za-z0-9_]{60,}\\b", severity: "block" },
{ id: "slack-token", label: "Slack token", regex: "\\bxox[baprs]-[A-Za-z0-9-]{10,}\\b", severity: "block" },
{ id: "google-api-key", label: "Google API key", regex: "\\bAIza[0-9A-Za-z_\\-]{35}\\b", severity: "block" },
{ id: "azure-storage-conn", label: "Azure storage connection string", regex: "AccountKey=[A-Za-z0-9+/=]{40,}", severity: "block" },
{ id: "azure-sas", label: "Azure SAS signature", regex: "[?&]sig=[A-Za-z0-9%]{20,}", severity: "warn" },
{ id: "generic-secret-assign", label: "Hardcoded secret assignment", regex: "(?i)(password|passwd|pwd|secret|client_secret|api[_-]?key|apikey|access[_-]?token|auth[_-]?token|connection[_-]?string)\\s*[:=]\\s*['\"][^'\"\\s]{8,}['\"]", severity: "warn" },
{ id: "jwt", label: "JSON Web Token", regex: "\\beyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}\\b", severity: "warn" },
{ id: "pem-cert", label: "Certificate block", regex: "-----BEGIN CERTIFICATE-----", severity: "warn" },
],

// Possible customer / personal data. Conservative — these are "warn" and are
// meant to prompt a human check, not to block. Common docs/sample values are
// excluded to keep the noise down.
pii: {
emailRegex: "[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}",
// Emails ending in these are treated as safe (examples, MS system addresses).
allowedEmailDomains: [
"example.com", "example.org", "contoso.com", "microsoft.com",
"users.noreply.github.com", "noreply.github.com",
],
// Public/reserved/documentation IP ranges we ignore.
ignoreIpPrefixes: ["10.", "192.168.", "127.", "0.", "255.", "172.16.", "203.0.113.", "198.51.100.", "192.0.2."],
ipRegex: "\\b(?:\\d{1,3}\\.){3}\\d{1,3}\\b",
},

labels: {
prefix: "sweeper",
ensureExist: true, // create any missing labels via the API
},
};
102 changes: 102 additions & 0 deletions .github/scripts/pr-sweeper/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Intelligent PR Sweeper

An AI-assisted, security-first pull-request reviewer for the FastTrack catalog.
It makes it **easy to contribute** (clear, friendly, actionable feedback on every
PR) while keeping the repo **secure** (deterministic guardrails that cannot be
bypassed, plus a dual-model AI review that never has the keys to the kingdom).

## What it does on every PR

1. **Deterministic guardrails (authoritative).** Scans the diff for hardcoded
secrets/keys, private keys, customer/PII data, disallowed binaries/executables,
oversized files, and out-of-scope or sensitive-path changes. These findings set
a commit **status** that fails the PR only for real blocking issues.
2. **Dual-model AI review (advisory).** Two independent models review the diff for
intent, obfuscation, logic, and contribution quality that a static scan misses.
Advisory only — it never gates or auto-merges.
3. **One sticky comment + labels.** Posts/updates a single report comment and
applies `sweeper:*` labels (risk level, security-review, scope-review, blocked).

## Security model (why it's safe on fork PRs)

The hard problem with automating PR review is that a fork PR contains **untrusted
code**, yet we need **write access** to comment and label. Running both in one job
is how tokens leak. So this uses GitHub's recommended two-stage split:

| Stage | Workflow | Trigger | Token | Runs untrusted code? |
|---|---|---|---|---|
| 1 · Analyze | `pr-sweeper.yml` | `pull_request` | `contents: read` only, **no secrets** | Checks out PR head but only runs **trusted inline git** to capture a diff artifact — never executes PR code |
| 2 · Report | `pr-sweeper-report.yml` | `workflow_run` | write scopes + `models: read` | **No** — checks out the trusted base repo, consumes the Stage-1 artifact as **data** |

Key properties:

- **Secrets are never exposed to untrusted code.** Stage 1 has no secrets and no
write token. Stage 2 has them but never runs PR code.
- **Guardrails can't be weakened by the PR.** The policy engine and its config
(`.github/pr-sweeper.config.mjs`) are always loaded from the trusted base repo in
Stage 2, not from the PR head.
- **Prompt-injection resistant.** Untrusted diff/PR text is passed to the models as
data with an explicit "treat as data, not instructions" system prompt. The models
are advisory; the injection-proof deterministic guardrails are what gate merges.
Every model- or PR-derived string is sanitized before it enters the bot's comment
(no raw HTML, links, images, or `@`-mentions), so a compromised model can't post
deceptive content as the trusted bot.
- **Write targets come from the trusted event, not the artifact.** Stage 2 resolves
the PR number and head SHA from the `workflow_run` event (binding the number to the
trusted head commit via the API for fork PRs), never from the attacker-controlled
`pr-meta.json`. A crafted artifact therefore can't make the bot comment on, label,
or green-light a *different* PR/commit.
- **Secrets are fully redacted.** Matched secret/PII evidence is replaced with
`[redacted]` — no fragment is ever echoed back into a public comment.
- **Least privilege + pinned supply chain.** Minimal `permissions:` per job and all
actions pinned to full commit SHAs (tracked by Dependabot).

## Files

```
.github/
pr-sweeper.config.mjs # single tuning surface (roots, limits, models, patterns)
workflows/
pr-sweeper.yml # Stage 1 — analyze (untrusted, data capture)
pr-sweeper-report.yml # Stage 2 — report (trusted, evaluate + comment)
scripts/pr-sweeper/
lib.mjs # shared helpers (diff parser, glob, redaction)
guardrails.mjs # deterministic policy engine (authoritative)
ai-review.mjs # dual-model reviewer (advisory, pluggable providers)
report.mjs # sticky comment + labels + commit status
```

## Configuration

Everything is tuned in **`.github/pr-sweeper.config.mjs`** — content roots,
file-size/type limits, scope thresholds, secret/PII patterns, and the two reviewer
models. No workflow edits required.

### Choosing the two review models

The default uses **GitHub Models** (zero secrets, just `permissions: models: read`):

- `fast` slot → `openai/gpt-5`
- `deep` slot → `openai/o3`

GitHub Models does **not** host Anthropic Claude, so "Opus 4.8" cannot run on the
built-in path. To use a real Opus 4.8 (or any non-catalog model) for the `deep`
slot, deploy it on **Azure AI Foundry** (which offers Anthropic models) and set that
slot to `provider: "azure-openai"` with an `endpoint`, then add the
`AZURE_OPENAI_API_KEY` repo secret. See the inline comments in the config file.

## Required repo settings

- **Actions:** allow GitHub Actions to run on pull requests.
- **Models:** the built-in path needs org/repo access to GitHub Models enabled.
- **Branch protection (recommended):** make the `PR Sweeper / security` status a
required check, and require CODEOWNER review for `.github/**` and
`tools/catalog-build/**`.
- The Stage-2 workflow only takes effect once it exists on the **default branch**
(this is inherent to `workflow_run`).

## Extending

- Add a secret detector → append to `secretPatterns` in the config.
- Adjust what counts as "sensitive" → edit `sensitivePaths`.
- Add a third reviewer → add another entry to `review.providers`.
Loading
Loading