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
9 changes: 7 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,11 @@ jobs:
- 24.x
- 26.x
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ matrix.node-version }}
cache: npm
Expand All @@ -31,3 +34,5 @@ jobs:
- run: npm run typecheck
- run: npm run build
- run: npm test
- name: Validate repository contracts
run: npm run --silent security:audit:ci
710 changes: 710 additions & 0 deletions .github/workflows/security-audit.yml

Large diffs are not rendered by default.

214 changes: 195 additions & 19 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
name: Security

on:
pull_request:
# This event loads the workflow definition from the protected base branch.
# The pull-request checkout below is data-only: no helper or project script is
# ever executed from it.
pull_request_target:
types:
- opened
- reopened
- synchronize
push:
branches:
- main
Expand All @@ -14,32 +21,201 @@ permissions:
jobs:
audit:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
- name: Checkout controller scripts from protected main
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false

- name: Checkout audited commit as data
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
path: target
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

# The audited checkout controls `.npmrc` and every non-manifest project
# file. Copy only the package manifests to a runner-owned directory and
# use empty runner-owned config files plus an explicit registry. The
# trusted validator rejects unsupported source forms before npm runs, and
# the audit stays lockfile-only: no target-controlled tarball is ever
# installed in this privileged job.
- name: Prepare isolated dependency workspace
env:
AUDIT_NPM_DIR: ${{ runner.temp }}/security-audit-npm
NPM_USER_CONFIG: ${{ runner.temp }}/security-audit-npm/user.npmrc
NPM_GLOBAL_CONFIG: ${{ runner.temp }}/security-audit-npm/global.npmrc
run: |
set -euo pipefail
rm -rf "${AUDIT_NPM_DIR}"
mkdir -p "${AUDIT_NPM_DIR}"
cp target/package.json target/package-lock.json "${AUDIT_NPM_DIR}/"
: > "${NPM_USER_CONFIG}"
: > "${NPM_GLOBAL_CONFIG}"

# `npm audit` prints advisory titles, severities, package names, versions
# and GHSA advisory URLs. On a public repository the Actions log and check
# result are world-readable. The trusted sanitizer validates the JSON and
# the reports are deleted unread. Exit 0 and 1 are then normalized to the
# same public result: npm uses 1 for findings, and exposing that result would
# disclose advisory existence. Invalid output and unexpected scanner exits
# still fail as operational errors.
- name: Run npm audit
shell: bash
working-directory: ${{ runner.temp }}/security-audit-npm
env:
NPM_USER_CONFIG: ${{ runner.temp }}/security-audit-npm/user.npmrc
NPM_GLOBAL_CONFIG: ${{ runner.temp }}/security-audit-npm/global.npmrc
run: |
set -uo pipefail
mkdir -p "${GITHUB_WORKSPACE}/.security-audit"
validate_status=0
node "${GITHUB_WORKSPACE}/scripts/security-audit/validate-npm-audit-inputs.mjs" \
--dir . \
> "${GITHUB_WORKSPACE}/.security-audit/npm-audit-inputs.log" 2>&1 ||
validate_status=$?
rm -f "${GITHUB_WORKSPACE}/.security-audit/npm-audit-inputs.log"
[ "${validate_status}" -eq 0 ] || exit 1
npm audit \
--package-lock-only \
--audit-level=high \
--json \
--registry=https://registry.npmjs.org/ \
--userconfig="${NPM_USER_CONFIG}" \
--globalconfig="${NPM_GLOBAL_CONFIG}" \
> "${GITHUB_WORKSPACE}/.security-audit/npm-audit.json" 2>/dev/null
status=$?
[ -f "${GITHUB_WORKSPACE}/.security-audit/npm-audit.json" ] ||
echo '{}' > "${GITHUB_WORKSPACE}/.security-audit/npm-audit.json"
sanitize_status=0
node "${GITHUB_WORKSPACE}/scripts/security-audit/sanitize-findings.mjs" \
--kind npm-audit \
--in "${GITHUB_WORKSPACE}/.security-audit/npm-audit.json" \
--out "${GITHUB_WORKSPACE}/.security-audit/npm-audit-summary.json" ||
sanitize_status=$?
rm -f \
"${GITHUB_WORKSPACE}/.security-audit/npm-audit.json" \
"${GITHUB_WORKSPACE}/.security-audit/npm-audit-summary.json"
[ "${sanitize_status}" -eq 0 ] || exit 1
if [ "${status}" -ne 0 ] && [ "${status}" -ne 1 ]; then
exit 1
fi
exit 0

- name: Discard dependency audit workspace
if: ${{ always() }}
env:
AUDIT_NPM_DIR: ${{ runner.temp }}/security-audit-npm
run: |
rm -rf "${AUDIT_NPM_DIR}"
rm -f .security-audit/npm-audit.json .security-audit/npm-audit-summary.json

# 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
- name: Checkout controller scripts from protected main
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.sha }}
persist-credentials: false

- name: Checkout audited commit as data with full history
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.event.pull_request.head.sha || github.sha }}
path: target
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. A
# trusted sanitizer validates the report and all report files are deleted
# unread. Exit 0 and 2 are normalized to the same public result so the check
# cannot disclose finding existence; any other exit remains an operational
# failure.
# 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 != '' }}
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 otherwise auto-loads configuration, ignore fingerprints,
# and inline allow comments from the scan target. Remove target-owned
# policy files and force the protected controller policy explicitly.
rm -rf -- target/.gitleaks.toml target/.gitleaksignore
./gitleaks git target \
--config scripts/security-audit/gitleaks-controller.toml \
--gitleaks-ignore-path scripts/security-audit/gitleaks-controller-ignore \
--ignore-gitleaks-allow \
--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
[ -f .security-audit/gitleaks.json ] || echo '[]' > .security-audit/gitleaks.json
sanitize_status=0
node scripts/security-audit/sanitize-findings.mjs \
--kind gitleaks \
--in .security-audit/gitleaks.json \
--out .security-audit/gitleaks-summary.json ||
sanitize_status=$?
rm -f .security-audit/gitleaks.json .security-audit/gitleaks-summary.json
[ "${sanitize_status}" -eq 0 ] || exit 1
case "${status}" in
0|2) exit 0 ;;
*) exit 1 ;;
esac

- name: Discard secret scan reports
if: ${{ always() }}
run: |
rm -f \
.security-audit/gitleaks.json \
.security-audit/gitleaks-summary.json \
.security-audit/gitleaks-console.log
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
46 changes: 46 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,52 @@ 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.** The complete weekly workflow is hard-disabled in code and
is not activation-ready. It is documented here so contributors know what a future, separately
reviewed version could do.

The repository contains security-audit workflow scaffolding with a declared weekly schedule; there
is no claim that a production schedule is active. No workflow job can run.
A future approved implementation 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 or signaled.** Validated findings have exactly one designed egress:
**GitHub Private Vulnerability Reporting**, where they are visible to repository maintainers
alone. Finding existence, scanner identity, counts, paths, private-submission outcome, and exploit
detail never appear in or influence public job/step names, conclusions, logs, artifacts, summaries,
pull request annotations, code scanning / SARIF, public issues, Azure DevOps, or IcM. There is no
fallback surface.
- **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.

The complete weekly workflow is intentionally inactive. Every job has a literal `false` activation
guard and the same generic public display name; it produces no audit summary. Repository variables,
secrets, or dispatch payloads cannot activate it. A future activation requires a reviewed code
change plus Private Vulnerability Reporting, a private operational-failure channel, public outcome
invariance, a protected environment, managed credentials, and all legal/privacy approvals.

The full design, boundaries and blocked 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
11 changes: 10 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,15 @@ 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 includes security-audit workflow scaffolding and credential-free local checks.
The complete public weekly workflow is hard-disabled and is **not activation-ready**: every job
has a literal `false` guard and one generic public display name, and there is no result summary.
Its proposed model runtime package is not yet approved or reproducible from the public npm registry,
so no lockfile is committed. Repository variables, secrets, and payloads cannot enable it. There is
no claim that a production weekly audit is active. See
[docs/SECURITY-AUDIT.md](docs/SECURITY-AUDIT.md) for safe local validation and the prerequisites
that remain open.

## Important notices

The MCP-specific notices and disclaimers for this project are consolidated in
Expand Down
Loading
Loading