Skip to content
Open
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
15 changes: 13 additions & 2 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,2 +1,13 @@
# <!-- The @microsoft/sharepoint-embedded team handle must be confirmed by the repo owner. -->
* @microsoft/sharepoint-embedded
# Code owners for microsoft/SharePoint-Embedded-MCP-Server.
#
# Entries must resolve to accounts or teams with write access to this
# repository, otherwise GitHub silently drops the rule and no review is ever
# requested. The previous `@microsoft/sharepoint-embedded` team handle did not
# resolve, so CODEOWNERS was effectively inert; these are direct collaborators.
* @dluces @marcwindle @pemtaira-msft

# Security-sensitive surfaces: workflows, audit tooling and control docs.
/.github/ @dluces @marcwindle @pemtaira-msft
/scripts/security-audit/ @dluces @marcwindle @pemtaira-msft
/docs/SECURITY-CONTROLS.md @dluces @marcwindle @pemtaira-msft
/docs/SECURITY-AUDIT.md @dluces @marcwindle @pemtaira-msft
11 changes: 11 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,14 @@ updates:
open-pull-requests-limit: 5
commit-message:
prefix: ci

# Pinned GitHub Copilot CLI used by the model-assisted security audit job.
# Kept out of the root manifest so it is never installed for normal builds
# and never published (root package.json "files" excludes tools/).
- package-ecosystem: npm
directory: /tools/copilot-cli
schedule:
interval: weekly
open-pull-requests-limit: 5
commit-message:
prefix: deps
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ jobs:
- 24.x
- 26.x
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node-version }}
cache: npm
Expand Down
693 changes: 693 additions & 0 deletions .github/workflows/security-audit.yml

Large diffs are not rendered by default.

146 changes: 130 additions & 16 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,32 +14,146 @@ permissions:
jobs:
audit:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x
cache: npm
- run: npm ci
- run: npm audit --audit-level=high
# `--ignore-scripts` keeps dependency lifecycle scripts (install/postinstall)
# from executing on a runner whose only job is to read the lockfile. `npm
# audit` resolves advisories from package-lock.json and does not need a
# built dependency tree, so nothing here depends on those scripts running.
- run: npm ci --ignore-scripts
# `npm audit` prints advisory titles, severities, package names, versions
# and GHSA advisory URLs. On a public repository the Actions log is
# world-readable, so that output would publish a machine-readable list of
# exploitable dependency paths before a fix exists. The JSON report is
# written to a file, reduced to counts by the sanitizer, and the raw report
# is deleted; the console never sees it.
- name: Run npm audit
id: audit
continue-on-error: true
shell: bash
run: |
set -uo pipefail
mkdir -p .security-audit
npm audit --audit-level=high --json > .security-audit/npm-audit.json 2>/dev/null
exit $?

- name: Reduce report to sanitized counts
shell: bash
run: |
set -euo pipefail
[ -f .security-audit/npm-audit.json ] || echo '{}' > .security-audit/npm-audit.json
node scripts/security-audit/sanitize-findings.mjs \
--kind npm-audit \
--in .security-audit/npm-audit.json \
--out .security-audit/npm-audit-summary.json
rm -f .security-audit/npm-audit.json

# Generic by design: no package, version, advisory identifier or URL. A
# maintainer reproduces locally with `npm audit --audit-level=high`.
- name: Fail if vulnerable dependencies were detected
if: steps.audit.outcome == 'failure'
shell: bash
run: |
echo "Security audit: FAIL — details were reported privately to maintainers." >&2
exit 1
Comment on lines +64 to +65

# Secret scanning that actually scans.
#
# The previous implementation used gitleaks/gitleaks-action gated on a
# GITLEAKS_LICENSE secret that was never provisioned, and additionally set
# continue-on-error, so the job could only ever report green without scanning
# anything. The Gitleaks CLI itself is MIT licensed and needs no licence key —
# only the marketplace action does — so the CLI is used directly, pinned by
# version and verified by SHA-256 before it is executed.
secrets:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
GITLEAKS_VERSION: '8.30.1'
# Provenance: taken from the upstream release artifact
# gitleaks_8.30.1_checksums.txt published at
# https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_checksums.txt
# (goreleaser-generated, published alongside the binaries). Re-verify this
# value against that file whenever GITLEAKS_VERSION is bumped.
GITLEAKS_SHA256: '551f6fc83ea457d62a0d98237cbad105af8d557003051f41f3e7ca7b3f2470eb'
steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
# gitleaks-action@v2 requires a GITLEAKS_LICENSE when run under a GitHub
# organization (free only for personal accounts). Until the owner
# provisions the secret, this step is skipped so the workflow stays green;
# it is also continue-on-error as a belt-and-suspenders. Owner action:
# add GITLEAKS_LICENSE (or switch to GitHub Advanced Security secret
# scanning, which is available org-wide) — see SECURITY.md.
persist-credentials: false

- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 24.x

- name: Download and verify gitleaks
shell: bash
run: |
set -euo pipefail
asset="gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz"
url="https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/${asset}"
curl --fail --silent --show-error --location --retry 3 --output "$asset" "$url"
echo "${GITLEAKS_SHA256} ${asset}" | sha256sum --check --strict
tar -xzf "$asset" gitleaks
chmod +x gitleaks
./gitleaks version

# `--exit-code 2` separates "leaks found" from an operational failure, so a
# crashed scanner can never be mistaken for a clean scan. The exit status is
# re-raised explicitly; it must not be swallowed by a trailing command.
# Console output is redirected and discarded unread: gitleaks prints one
# block per finding carrying file path, line, commit, author and e-mail.
# `--redact` masks only the secret value, not that metadata, and Actions
# logs are world-readable on a public repository.
- name: Scan for secrets (gitleaks)
if: ${{ env.GITLEAKS_LICENSE != '' }}
id: scan
continue-on-error: true
uses: gitleaks/gitleaks-action@v3
env:
GITLEAKS_ENABLE_COMMENTS: "false"
shell: bash
run: |
set -uo pipefail
mkdir -p .security-audit
./gitleaks git . \
--report-format json \
--report-path .security-audit/gitleaks.json \
--redact \
--exit-code 2 \
--no-banner \
> .security-audit/gitleaks-console.log 2>&1
status=$?
rm -f .security-audit/gitleaks-console.log
exit "${status}"

# The raw report carries match context and commit metadata, so it is reduced
# to counts only — no rule identifiers, no file paths — and the original is
# deleted. See scripts/security-audit/sanitize-findings.mjs.
#
# The counts are NOT written to the job summary: a job summary on a public
# repository is world-readable, and a non-zero count is itself a public
# signal that an unfixed secret exists in this history.
- name: Reduce report to sanitized counts
shell: bash
run: |
set -euo pipefail
[ -f .security-audit/gitleaks.json ] || echo '[]' > .security-audit/gitleaks.json
node scripts/security-audit/sanitize-findings.mjs \
--kind gitleaks \
--in .security-audit/gitleaks.json \
--out .security-audit/gitleaks-summary.json
rm -f .security-audit/gitleaks.json

# Generic by design. A maintainer reproduces locally with
# `gitleaks git . --redact --no-banner`; rotate any exposed credential
# before removing it from history.
- name: Fail if secrets were detected
if: steps.scan.outcome == 'failure'
shell: bash
run: |
echo "Security audit: FAIL — details were reported privately to maintainers." >&2
exit 1
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ dist/
coverage/
*.tgz

# Security audit run outputs (corpus, model report, scanner reports) — never committed
.security-audit/

# Sample app build outputs (the sample SOURCES under samples/ are committed)
samples/**/bin/
samples/**/obj/
Expand Down
43 changes: 43 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,49 @@ For more information see the
[opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or
comments.

## Optional model-assisted security analysis

**This feature is disabled by default.** It is documented here so contributors know what *could*
happen to code they contribute, if maintainers ever enable it.

The repository contains a scheduled weekly security-audit workflow. It has an optional stage that,
when a maintainer explicitly enables it, may send a bounded selection of **already-public,
git-tracked source files from `main`** to **GitHub Copilot**, which relays them to a **third-party
model provider** for advisory security analysis.

What this stage does and does not do:

- **Only public, tracked source.** The corpus is limited to an allowlist of source file extensions
from committed files on `main`, under a hard file-count and byte cap. Untracked files, local
working-tree changes, build output and dependencies are never included.
- **No separate repository or activity data.** The corpus does not query issues, pull requests,
discussions, commit messages, author records, CI logs or the runner environment. It does include
each selected file's repository-relative path, line count and public source content, which may
itself contain names, identifiers, credential-shaped strings or environment-variable references.
- **No tools, no writes.** The model runs without tools, without MCP servers, without shell access
and without any write permission. It cannot open issues, comment, push, or change settings.
- **Advisory and redacted.** Output is schema-validated and redacted before use, is advisory only,
and is never a required check for merging a pull request.
- **Never published.** Validated findings are submitted only through **GitHub Private Vulnerability
Reporting**, where they are visible to repository maintainers alone. They are never written to
job logs, workflow artifacts, job summaries, pull request annotations, code scanning / SARIF,
public issues, Azure DevOps or IcM. There is no fallback surface: if private reporting is
unavailable the audit fails closed and publishes nothing. The only public output of an audit run
is `Security audit: PASS` or
`Security audit: FAIL — details were reported privately to maintainers.`
- **Never triggered by contributions.** The workflow has no `pull_request` or
`pull_request_target` trigger. Opening or updating a pull request never sends anything anywhere.

Activation is gated on more than a single switch: a maintainer must enable Private Vulnerability
Reporting on the repository, provision a protected environment and credential for submission, and
set two separate opt-in repository variables. Any one of those being absent leaves the stage off.

The full design, boundaries and activation prerequisites are documented in
[docs/SECURITY-AUDIT.md](docs/SECURITY-AUDIT.md).

If you have concerns about this feature as it relates to your contribution, please open a GitHub
discussion or a non-security issue and a maintainer will discuss it with you.

## Reporting security issues

Please report security issues privately as described in [SECURITY.md](SECURITY.md). Do
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ Prefer the command line? Run `claude mcp add spe -- npx -y @microsoft/spe-mcp st

- **Get started on Microsoft Learn:** [SharePoint Embedded MCP server](https://learn.microsoft.com/sharepoint/dev/embedded/getting-started/spe-mcp-server)
- **SharePoint Embedded product docs:** <https://learn.microsoft.com/sharepoint/dev/embedded/>
- **In this repo:** [Available Tools](#available-tools) · [Configuration](#configuration) · [Security controls](docs/SECURITY-CONTROLS.md) · [Troubleshooting](docs/TROUBLESHOOTING.md)
- **In this repo:** [Available Tools](#available-tools) · [Configuration](#configuration) · [Security controls](docs/SECURITY-CONTROLS.md) · [Security audit](docs/SECURITY-AUDIT.md) · [Troubleshooting](docs/TROUBLESHOOTING.md)

## Available Tools

Expand Down Expand Up @@ -547,6 +547,11 @@ Microsoft takes security seriously. If you believe you have found a security
vulnerability, please report it privately as described in [SECURITY.md](SECURITY.md) —
**do not** file a public GitHub issue.

This repository runs a scheduled weekly security audit (CodeQL, dependency audit, secret
scanning, and action-pin enforcement, plus an optional model-assisted review layer). See
[docs/SECURITY-AUDIT.md](docs/SECURITY-AUDIT.md) for how to run it, how to triage results,
and the administrative steps required to enable the model-assisted layer.
Comment on lines +550 to +553

## Important notices

The MCP-specific notices and disclaimers for this project are consolidated in
Expand Down
37 changes: 36 additions & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,39 @@ For security reporting information, locations, contact information, and policies
please review the latest guidance for Microsoft repositories at
[https://aka.ms/SECURITY.md](https://aka.ms/SECURITY.md).

<!-- END MICROSOFT SECURITY.MD BLOCK -->
<!-- END MICROSOFT SECURITY.MD BLOCK -->

## Private reporting on this repository

This repository uses **GitHub Private Vulnerability Reporting (PVR)**. Reports submitted through
PVR are visible only to repository maintainers — never in public issues, pull request comments,
job logs, workflow artifacts, or the public code scanning surface.

To report a vulnerability you found yourself, use **Security → Report a vulnerability** on this
repository, or follow the Microsoft guidance linked above. Do not open a public issue.

### Automated audit reports

The repository's optional model-assisted security audit
(see [docs/SECURITY-AUDIT.md](docs/SECURITY-AUDIT.md)) submits its validated findings through the
**same** PVR endpoint, and through no other channel. Specifically:

- Automated findings and any exploit detail are **never** written to job logs, workflow artifacts,
job summaries, pull request annotations, code scanning / SARIF, public issues, Azure DevOps, or
IcM. There is no fallback surface: if private reporting is unavailable, the audit fails closed
and publishes nothing.
- Each audited commit produces at most **one aggregate report**, titled
`SPE automated security audit — <first 12 hex of the audited commit SHA>`.
- Submission is de-duplicated against existing reports in the `triage` and `draft` states by exact
title match, so re-running the audit for the same commit does not create a duplicate report.
- Reports are drafted as repository security advisories in the private reporting queue and are
therefore visible only to maintainers. They are advisory input for human triage; they are not
published advisories and they never gate a pull request.

Public workflow output for a security audit run is limited to one of two literals:
`Security audit: PASS` or
`Security audit: FAIL — details were reported privately to maintainers.`

The model-assisted stage is **disabled by default** and requires explicit maintainer activation,
including PVR being enabled on the repository. See
[docs/SECURITY-AUDIT.md](docs/SECURITY-AUDIT.md) for the full activation prerequisites.
Loading
Loading