@a-kazemi/sppa
v0.2.1
Published
Explain and audit SharePoint Server on-premises permissions from the command line. Read-only, no data egress, no telemetry.
Maintainers
Readme
sppa
Explain and audit SharePoint Server on-premises permissions from the command line. Built for SharePoint Server 2016, 2019 and Subscription Edition farms.
explain-access— answer "why does this user have access to this site?" in one command: direct grants, SharePoint group membership, broad-audience claims ("Everyone"), AD security groups, and exactly where inheritance breaks.list-access— the inverse: "who has access to this site or list, and how?" SharePoint groups are expanded to their members; AD security groups and broad audiences are flagged as unexpandable.scan-site— audit one site collection (add--recursefor subwebs) for broken permission inheritance, orphaned SIDs (deleted AD accounts still on ACLs), broad-audience grants, oversized SharePoint groups, and site collection administrators.
Read-only. No data leaves your machine. No telemetry. No account required.
The tool only issues GET requests to the SharePoint REST API (_api).
Status:
v0.2.1, early release. It does one job on classic NTLM farms. If it is useful — or if it breaks in your environment — please open an issue.
Install
Requires Node.js 18 or newer.
npm install -g @a-kazemi/sppa
sppa --helpOr run it without installing:
npx @a-kazemi/sppa scan-site --site https://sharepoint/sites/hrFrom source (for development):
git clone https://github.com/a-kazemi/sppa.git
cd sppa
npm install && npm run build
npm link # puts `sppa` on your PATHAuthentication
The v0.2.x line supports NTLM with explicit credentials only (see docs/AUTH.md;
auth failures are catalogued in docs/TROUBLESHOOTING.md).
Provide the auditing account through environment variables — never on the command
line, where it would show up in the process list:
# macOS / Linux (bash, zsh)
export SPPA_USERNAME='CONTOSO\svc_audit'
export SPPA_PASSWORD='...'
# optional; also parsed from DOMAIN\user above
export SPPA_DOMAIN='CONTOSO'# Windows — PowerShell
$env:SPPA_USERNAME = 'CONTOSO\svc_audit'
$env:SPPA_PASSWORD = '...'
$env:SPPA_DOMAIN = 'CONTOSO'REM Windows — Command Prompt (cmd.exe); do not quote the values
set SPPA_USERNAME=CONTOSO\svc_audit
set SPPA_PASSWORD=...
set SPPA_DOMAIN=CONTOSOexport is Unix-only — on Windows use set (cmd.exe) or $env: (PowerShell).
See docs/AUTH.md for details.
A read-only account works for everything except item-level scanning inside lists where it lacks access — those items are simply skipped.
Usage
Explain why a user has access
sppa explain-access --site https://sharepoint/sites/hr --user 'CONTOSO\a.kazemi'SharePoint access explanation
Site: https://sharepoint/sites/hr
User: Jane Doe (User, id 14)
Login: i:0#.w|contoso\a.kazemi
Verdict: HAS ACCESS
Effective permissions on this scope:
• Open the site
• View pages
• View list items / documents
• Edit list items
Inheritance is broken at:
• list: Salary Review
How access is granted:
✓ SharePoint group "HR Site Members" — Contribute @ list "Salary Review"
Notes:
• 1 AD security group(s) on this scope could not be expanded via REST.
The effective-permission check above is still authoritative.Scope it to a single list, or handle classic Windows-claims logins:
sppa explain-access --site https://sharepoint/sites/hr \
--user 'CONTOSO\a.kazemi' --list 'Salary Review' --windows-claimsList who has access
sppa list-access --site https://sharepoint/sites/hr
sppa list-access --site https://sharepoint/sites/hr --list 'Salary Review'list-access reads the role assignments on the scope (or the parent it inherits
from), expands every SharePoint group to its members, and reports AD security
groups and broad audiences ("Everyone") as single entries it cannot expand. See
sample/list-access.txt for the shape of the output.
Audit a site collection
sppa scan-site --site https://sharepoint/sites/hr
sppa scan-site --site https://sharepoint/sites/hr --recurse
sppa scan-site --site https://sharepoint/sites/hr --format json > hr-audit.jsonscan-site walks the web, every visible list, and (unless --skip-items) list
items with unique permissions. Add --recurse to walk sub-sites too — every
finding is then tagged with the subweb it came from and the summary reports how
many webs were scanned. Use --max-items to bound very large libraries and
--large-group-threshold to tune the oversized-group flag.
Unless --format json is used, scan-site also writes a standalone HTML report
(inline CSS, no assets, prints cleanly to PDF) and prints a Report: link to it
as the last line of the terminal output. The default path is
./sppa-scan-<host>-<timestamp>.html; override it with --report <path> or turn
it off with --no-report. To re-render a report from a saved
--format json file without re-scanning, pipe it through
scripts/scan-report.mjs.
NTLM authenticates one TCP connection, so by default every request is serial.
On a large site collection, --concurrency 4 (say) opens four authenticated
connections and runs the scan several times faster. The client also retries
farm throttling (429 / 503, honouring Retry-After) and transient socket
drops automatically.
Worked example
Not ready to point it at a real farm yet? sample/ contains a
synthetic SharePoint Server 2019 site collection — recorded _api responses plus
the exact scan-site and explain-access output the tool produces from them,
showing every finding (broken inheritance at web/list/item, an orphaned SID, an
Everyone grant, an unexpandable AD group, an oversized group, a site collection
admin). It also has both an ALLOW trace (explain-access.txt) and a DENY
trace scoped to a single list (explain-access-deny.txt). A test regenerates it
from the fixtures on every run, so it never drifts from the code.
For a line-by-line walk through that sample output — the ALLOW trace, the DENY
trace, and scan-site — see docs/GUIDE-explain-access.md.
JSON output
Both commands accept --format json and emit a stable envelope
(schemaVersion: 1) suitable for diffing between runs or feeding into a report:
{
"tool": "sppa",
"schemaVersion": 1,
"command": "scan-site",
"generatedAt": "2026-09-03T12:00:00.000Z",
"site": "https://sharepoint/sites/hr",
"result": { "summary": { "...": "..." }, "brokenInheritance": [] }
}What it does not do (yet)
- No AD FS / WS-Federation or Forms-Based Auth (NTLM only).
- No Kerberos-only endpoints.
- No AD security-group expansion — SharePoint REST does not expose it. The tool
reports which AD groups are on each ACL and relies on
getUserEffectivePermissions(which resolves them server-side) for the verdict. - No writes, ever. It will not fix anything it finds.
- No multi-farm, scheduled snapshots, or drift diffing.
Where those gaps sit in the queue — and everything else that has been asked for — is written down in docs/ROADMAP.md. Listing something there is not a commitment to build it; open an issue to make the case for what you need.
Exit codes
| Code | Meaning | |-----:|---------| | 0 | Success | | 2 | Usage error (bad flags / missing arguments) | | 3 | Authentication failed | | 4 | SharePoint reachable but the request failed | | 5 | Network / TLS / DNS failure |
Every non-zero exit prints one error: line and usually a hint.
docs/TROUBLESHOOTING.md lists each message with its
cause and fix — including cases with no dedicated message (clock skew, channel
binding / Extended Protection, _api disabled, reverse proxies, multi-WFE
affinity).
Security
See docs/SECURITY.md. Short version: read-only, single farm,
no outbound connections other than to the --site you pass, credentials read
from the environment and never logged.
Feedback wanted — and a free permission audit
This is an early release and the fastest way to make it better is to hear from people running real farms.
- Hit a bug or an auth failure? Open an issue with your SharePoint version and the (redacted) error — the Authentication failure report form is the one we most want filled in right now.
- Have a messy permission situation you'd like a second pair of eyes on?
Use the free permission audit
form (no data required) and we'll help you read the
explain-access/scan-siteoutput and figure out what to fix — free, no strings. We're doing this to learn which problems matter most.
Development
npm install
npm run build
npm test # builds, then runs the node:test suiteThe permission-analysis logic (src/analysis/) and the NTLM handshake
(src/auth/) are pure functions unit-tested against recorded REST fixtures and
the [MS-NLMP] test vectors — no live farm needed to hack on them.
test/sample.test.ts runs the whole _api → parse → analyse → render pipeline
against the synthetic farm in sample/ and fails if the committed
output there goes stale.
See CONTRIBUTING.md for what is in and out of scope — v0.2.x
is feature-frozen; bug fixes, docs, tests, and auth reports are what move it
forward.
License
MIT
