A PCI DSS compliance system implementing requirements 6.4.3 (Script Management) and 11.6.1 (Detection and Alerting) to prevent page tampering and e-skimming attacks on payment pages.
Note: This repository is largely agent developed.
Run with minimal required parameters:
npm start -- --repo https://github.com/org/inventory --git-token <YOUR_TOKEN>This runs both inventory and detection workflows against all configured targets using default branches.
Run all workflows with Slack alerts:
npm start -- \
--repo https://github.com/org/inventory \
--git-token <YOUR_TOKEN> \
--slack-token <YOUR_SLACK_TOKEN>If you have your tokens in .env.secrets (see below for setup):
source .env.secrets
npm start -- \
--repo $INVENTORY_REPO_URL \
--git-token $INVENTORY_REPO_PAT \
--slack-token $SLACK_OAUTH_TOKEN \
--git-user-name $GIT_USER_NAME \
--git-user-email $GIT_USER_EMAILRun inventory only for a specific target:
npm start -- \
--mode inventory \
--target 1.0 \
--repo https://github.com/org/inventory \
--git-token <YOUR_TOKEN>Run detection only against production:
npm start -- \
--mode detection \
--repo https://github.com/org/inventory \
--git-token <YOUR_TOKEN> \
--slack-token <YOUR_SLACK_TOKEN>Use custom branches for inventory and detection:
npm start -- \
--repo https://github.com/org/inventory \
--git-token <YOUR_TOKEN> \
--inventory-branch inventory-updates \
--detection-branch mainLocal testing with file:// protocol:
npm start -- \
--repo file:///path/to/local/inventory \
--git-token dummyThe system runs one of four modes via --mode:
inventory— visits staging/inventory URLs, discovers scripts and headers, pushes updates to theinventory-updatesbranch of the inventory repo, and opens a PR for review. Alerts on resources that need manual authorization.detection— visits production/detection URLs, compares what's loaded against the approved inventory onmain, and alerts on anything unauthorized. Read-only against the inventory repo.all(default) — runsinventory, thendetection.validate— runs as a CI check inside the inventory repo. Fully deserializes everytargets/*.json(Zod schema,createMatcher(), workflow resolution) so malformed inventory cannot merge. No browser, no alerts, no push.
The intended day-to-day cycle:
- Inventory mode (against staging) discovers new scripts/headers, pushes them to
inventory-updates, and opens a PR. - A human reviews the PR, adds authorization metadata for legitimate resources, and merges to
main. - Detection mode (against production) reads from
mainand alerts on anything unauthorized.
See Branch Usage for the branch model and CI Validation for the Inventory Repo for the CI wiring.
| Parameter | Description | Example |
|---|---|---|
--repo <url> |
Inventory repository URL (HTTPS or file://) | https://github.com/org/inventory |
--git-token <token> |
Git authentication token (required for HTTPS; optional only for --mode validate with a file:// repo) |
${{ secrets.GITHUB_TOKEN }} |
| Parameter | Description | Default |
|---|---|---|
--mode <mode> |
Execution mode: inventory, detection, all, or validate |
all |
--target <name> |
Process specific target (e.g., "1.0") | all targets |
--slack-token <token> |
Slack token for alerts (logs to console if omitted) | - |
--inventory-branch <name> |
Branch for inventory operations | inventory-updates |
--detection-branch <name> |
Branch for detection operations | main |
--git-user-name <name> |
Git committer name for inventory updates | PCI DSS Page Tampering Bot |
--git-user-email <email> |
Git committer email for inventory updates | noreply@example.com |
--help |
Display help message and exit | - |
The system uses different branches for different purposes:
- Purpose: Updates baseline inventory with newly discovered scripts/headers
- Default:
inventory-updates - Behavior: Reads from and pushes changes to this branch
- Use case: Staging/development environment monitoring to update approved resource list
- Purpose: Read-only comparison against stable inventory
- Default:
main - Behavior: Reads from this branch, never pushes changes
- Use case: Production monitoring against approved baselines
-
Inventory workflow →
inventory-updatesbranch- Runs against staging/inventory URLs
- Adds new scripts/headers as they're discovered
- Creates alerts for resources needing manual authorization
-
Detection workflow →
mainbranch- Runs against production/detection URLs
- Compares against stable, reviewed inventory
- Alerts on any unauthorized changes
-
Review process:
- Review changes in
inventory-updatesbranch - Add authorization metadata for legitimate resources
- Merge to
mainafter approval - Detection workflow now recognizes these resources as authorized
- Review changes in
# Step 1: Run inventory to discover new resources
npm start -- \
--mode inventory \
--inventory-branch inventory-updates \
--repo https://github.com/org/inventory \
--git-token <TOKEN>
# Step 2: Review and approve changes in inventory-updates branch
# (Manual review via pull request or direct commits)
# Step 3: Run detection against approved baseline
npm start -- \
--mode detection \
--detection-branch main \
--repo https://github.com/org/inventory \
--git-token <TOKEN> \
--slack-token <SLACK_TOKEN>For GitHub Actions, pass secrets via CLI parameters:
- name: Run PCI DSS monitoring
run: |
npm start -- \
--repo https://github.com/${{ github.repository }}-inventory \
--git-token ${{ secrets.INVENTORY_REPO_PAT }} \
--slack-token ${{ secrets.SLACK_TOKEN }} \
--inventory-branch inventory-updates \
--detection-branch main \
--git-user-name 'PCI DSS Bot' \
--git-user-email 'pci-bot@example.com'Three GitHub Actions workflows ship with this repo under .github/workflows/:
ci.yml — Continuous Integration
Runs on every push to main, every pull request, and on manual dispatch. Installs dependencies, audits them at --audit-level=high, then runs linting, type checking, unit tests, and integration tests on Node 24. This is the gate that protects main.
inventory-and-detection.yml — Scheduled monitoring
The production runner. Triggers:
- Scheduled: daily at 12:00 UTC (overnight in AU). Runs
--mode allagainst every target. workflow_dispatch: manual run with optionalmode(all/inventory/detection) andtargetinputs — useful for ad-hoc inventory sweeps or re-running detection after a fix.- Push to
main: runs after merges so newly-approved inventory takes effect immediately.
Requires repo secrets INVENTORY_REPO_PAT and SLACK_OAUTH_TOKEN, and repo variables INVENTORY_REPO_URL, GIT_USER_NAME, GIT_USER_EMAIL. Installs Chrome system dependencies for Puppeteer before invoking npm start.
auto-merge-renovate.yml — Renovate auto-merge
Listens for completed CI runs (via workflow_run) and, when the run was triggered by a renovate[bot] PR and succeeded, approves and squash-merges the PR. Gating on workflow_run (rather than pull_request) ensures CI has actually passed before merging — the previous pull_request setup let broken lockfiles land on main.
For wiring --mode validate into the inventory repo's CI (a separate repo), see CI Validation for the Inventory Repo below.
The validate mode is designed to run as a pre-merge CI check in the script-inventory repository. It exercises the same code paths the runtime tool uses to load inventory files, so anything that passes CI will also load in production.
- Clones the inventory repo (supports
file://for the CI's local checkout) and switches to the requested branch. - Reads every
targets/*.jsonfile. - Parses each file with
RawInventorySchema(catches bad regex patterns, missing fields, malformed hashes, unsupported matcher shapes). - Runs
createMatcher()on everyidentifyWithandauthoriseWithtree (catches any matcher construction failures that slip past schema). - Resolves every
workflowreference viaWorkflowDefinitionSchema(catches dangling workflow files and malformed workflow definitions). - Exits 0 on success, or non-zero with a contextual error message on failure.
It does not launch Puppeteer, hit the monitored URLs, send alerts, or push any changes.
Against a local checkout of the inventory repo:
npm start -- --mode validate --repo file://$PWD--git-token is not required when --repo is a file:// URL in validate mode.
| Code | Meaning |
|---|---|
| 0 | All inventory files fully deserialize |
| 1 | CLI argument validation error (malformed --repo, missing --git-token for HTTPS, etc.) |
| 2 | Inventory or execution error (schema failure in an inventory file, invalid regex, malformed matcher, missing workflow file, clone failure) |
For inventory-file validation failures, exit-2 messages name the offending file — e.g. Validation failed for inventory file '1.0.json': Invalid regex in nameMatcher at "scripts.0.identifyWith.nameMatcher". Pre-read failures (clone failures, branch checkout errors) surface the underlying git error without a file qualifier.
Check out this tool alongside the inventory repo and run validate mode against the inventory's working tree. Pass GITHUB_HEAD_REF as --inventory-branch so the validation runs against the PR branch rather than the default branch.
jobs:
validate-inventory:
runs-on: ubuntu-latest
steps:
- name: Checkout inventory repo
uses: actions/checkout@v4
with:
path: inventory
fetch-depth: 0
- name: Checkout validation tool
uses: actions/checkout@v4
with:
repository: mr-yum/pci-dss-page-tampering
path: tool
- name: Install tool dependencies
working-directory: ./tool
run: npm ci
- name: Validate inventory
working-directory: ./tool
env:
INVENTORY_BRANCH: ${{ github.head_ref || github.ref_name }}
run: |
npm start -- \
--mode validate \
--repo file://$GITHUB_WORKSPACE/inventory \
--inventory-branch "$INVENTORY_BRANCH"Notes:
fetch-depth: 0on the inventory checkout ensures all branches are available so simple-git can clone fromfile://and switch to the PR branch.github.head_refis only set onpull_requestevents;github.ref_namecovers direct pushes. The example falls back between the two.- If the inventory repo's CI needs to validate
mainrather than the PR branch, omit--inventory-branch(defaults toinventory-updates) or passmainexplicitly.
Requires .env.secrets file:
# .env.secrets
INVENTORY_REPO_PAT=<PAT secret>
Run locally:
act push --container-architecture linux/amd64 --secret-file .env.secretsEach inventory file (targets/<name>.json) lists the scripts and headers approved for a target. Each entry uses two matchers:
identifyWith— picks out the script or header (e.g. by URL or header name)authoriseWith— describes what content/hash is acceptable, withauthorisationInfometadata
{
"identifyWith": { "nameMatcher": "^https://cdn\\.example\\.com/analytics\\.js$" },
"authoriseWith": {
"hashes": [{ "timestamp": "2025-10-21T12:00:00.000Z", "hash": { "value": "abc..." } }],
"authorisationInfo": {
"description": "Analytics script for conversion tracking",
"authorised": true,
"date": "2025-10-21T12:00:00.000Z"
}
}
}For complex authorization policies, authoriseWith supports composite matchers:
- AND Matcher: authorize only if ALL children succeed (e.g. CSP with multiple required directives)
- OR Matcher: authorize if ANY child succeeds (e.g. accept production OR staging policy)
- Array syntax: syntactic sugar for OR matcher (multiple acceptable versions)
AND Matcher (CSP with multiple required directives):
{
"identifyWith": { "headerNameMatcher": "^content-security-policy$" },
"authoriseWith": {
"andMatcher": [{ "contentMatcher": "default-src\\s+https:" }, { "contentMatcher": "script-src\\s+https:" }, { "contentMatcher": "object-src\\s+'none'" }],
"authorisationInfo": {
"description": "CSP requiring all three critical directives",
"authorised": true,
"date": "2025-10-24T12:00:00.000Z"
}
}
}OR Matcher (accept multiple acceptable policies):
{
"orMatcher": [{ "contentMatcher": "default-src\\s+https:.*script-src\\s+https:" }, { "contentMatcher": "default-src\\s+'self'.*script-src\\s+'self'" }, { "contentMatcher": "default-src\\s+'none'" }],
"authorisationInfo": {
"description": "Accept production, staging, or maintenance policies",
"authorised": true,
"date": "2025-10-24T12:00:00.000Z"
}
}Array syntax (multiple script versions):
{
"identifyWith": { "nameMatcher": "^https://cdn\\.example\\.com/analytics\\.js$" },
"authoriseWith": [
{
"hashes": [{ "timestamp": "2025-10-01T00:00:00.000Z", "hash": { "value": "abc..." } }],
"authorisationInfo": { "description": "Version 1.0.0", "authorised": true, "date": "2025-10-01T00:00:00.000Z" }
},
{
"hashes": [{ "timestamp": "2025-10-15T00:00:00.000Z", "hash": { "value": "def..." } }],
"authorisationInfo": { "description": "Version 1.1.0", "authorised": true, "date": "2025-10-15T00:00:00.000Z" }
}
]
}To validate every inventory file in a local checkout of the inventory repo, use --mode validate:
npm start -- --mode validate --repo file://$PWDValidate mode runs the full deserialization pipeline used at runtime — Zod schema parsing, createMatcher() construction for every identifyWith/authoriseWith tree, and workflow file resolution — so anything that parses here will also load at production execution time. See CI Validation for the Inventory Repo above for the GitHub Actions wiring.
| Error | Solution |
|---|---|
| Invalid regex pattern | Test regex: new RegExp("your-pattern") |
| Missing required field | Add both identifyWith and authoriseWith |
| Invalid SHA256 hash | Ensure 64 lowercase hex characters |