Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

rsscan

PyPI is the published version of record. Block commits and builds that introduce API keys, tokens and other machine credentials.

Detects 31 credential patterns: AWS IAM keys, GitHub PATs, Stripe secrets, Slack tokens, private keys, and LLM provider keys (OpenAI, Anthropic, Google, Groq, xAI, Replicate).

Free, and it runs entirely on your machine. No account, no API key, no network call. Your source code never leaves the host. Matching happens locally against patterns shipped inside the package.

The pre-commit hook is the point. It runs before the commit enters git history. A CI check only sees the secret after a push, and by then it is in history and has to be rotated even if you delete the commit. The CI integrations below are a backstop, not a substitute.

New in 0.2.0:rsscan --deps counts the accounts that can publish into your npm dependencies. See Counting who can publish your dependencies.

Install

pre-commit hook (recommended)

# .pre-commit-config.yamlrepos:
- repo: https://github.com/RelayShield/rsscanrev: v0.2.0hooks:
- id: rsscan
pre-commit install

That is the whole setup. Nothing to configure, nothing to sign up for.

GitHub Actions

- uses: actions/checkout@v4with:
fetch-depth: 0# required: the range needs history
- uses: RelayShield/rsscan@v0.2.0with:
fail-on: HIGH

Findings are annotated inline on the changed lines in the pull request's Files tab, so a developer sees them where the code is rather than in collapsed build output. Blocking findings (at or above fail-on) appear as errors; everything else appears as warnings.

This needs no token, no permissions: block and no GitHub App. Annotations are emitted as workflow commands, not through the Checks API. Turn them off with annotate: off if you only want the log.

Annotations never contain the secret value. Each one carries the credential type, its severity, and the fingerprint you would add to .relayshield-allowlist to suppress a false positive.

GitLab CI/CD

rsscan:
image: relayshield/rsscan:0.2.0variables:
GIT_DEPTH: 0# required: GitLab shallow-clones, and a shallow clone# yields an empty diff, so the job would pass having# scanned nothingRSSCAN_REV_RANGE: "origin/$CI_DEFAULT_BRANCH...HEAD"RSSCAN_FAIL_ON: HIGHscript: ["rsscan"]

CircleCI

orbs:
rsscan: relayshield/rsscan@0.1.0workflows:
main:
jobs:
- rsscan/scan

Docker: Bitbucket Pipelines, Tekton, Drone, Woodpecker, Harness, anything else

docker run --rm -v "$PWD:/workspace" \
-e RSSCAN_REV_RANGE=origin/main...HEAD \
relayshield/rsscan:0.2.0

Bitbucket Pipelines:

- step:
script:
- pipe: docker://relayshield/rsscan:0.2.0variables:
RSSCAN_REV_RANGE: "origin/main...HEAD"

Jenkins, Azure DevOps, or any shell

pip install rsscan
RSSCAN_REV_RANGE="origin/main...HEAD" rsscan

How it works

Scans the diff locally and fails on a finding at or above --fail-on (default HIGH).

Only added lines are scanned. Secrets already in your files are not re-flagged, so the tool does not become unbypassable noise on a repo with legacy findings.

Nothing is transmitted and nothing is printed. There is no scan endpoint to send code to. Matched values never appear in output either: findings carry a file, a line and a non-reversible fingerprint, so a scan is safe to run in CI without leaking the secret into build logs.

 rsscan: secrets detected in staged changes
CRITICAL AWS IAM Access Key
src/config.py:14
fingerprint sha256:1a5d44a2dca19669
Commit refused.
Remove the value and load it from a secrets manager or environment
variable instead. If it has already left this machine, rotate it.
False positive? Add the fingerprint to .relayshield-allowlist:
echo 'sha256:1a5d44a2dca19669' >> .relayshield-allowlist
To bypass entirely: git commit --no-verify

Configuration

Every flag has an environment variable equivalent, which is how the CI clients drive it.

FlagEnv varDefaultMeaning
--fail-onRSSCAN_FAIL_ONHIGHLowest severity that fails. LOW/MEDIUM/HIGH/CRITICAL.
--rev-rangeRSSCAN_REV_RANGE(staged)Scan a commit range instead of staged changes.
--allowlistRSSCAN_ALLOWLIST.relayshield-allowlistFingerprints to ignore.
--reportRSSCAN_REPORT(off)Write a shareable exposure report to this path.
--orgRSSCAN_ORG(off)Opt in to reporting adoption for your org domain.
--strict / --no-strictRSSCAN_STRICTsee belowFail when the scan cannot run.
--annotateRSSCAN_ANNOTATEautoInline PR annotations. auto enables them inside GitHub Actions only; github forces; off disables.
--slack-webhookRSSCAN_SLACK_WEBHOOK(off)POST a findings summary to your own Slack incoming webhook.
--webhookRSSCAN_WEBHOOK(off)POST findings as JSON to an endpoint you control.

Failure behaviour differs by mode, on purpose

Pre-commit fails open. If the scan genuinely cannot run (an unreadable diff, a broken git invocation), it warns on stderr and lets the commit through. A hook that wedges every commit gets uninstalled, and then it catches nothing.

CI fails closed. A gate that silently reports success when it could not actually run is worse than no gate. It manufactures false assurance. Override either way with --strict / --no-strict or RSSCAN_STRICT.

Allowlisting

The allowlist holds fingerprints, not secrets: a file containing the actual values would be the same mistake this tool exists to prevent.

# .relayshield-allowlist
sha256:1a5d44a2dca19669 # documented example key in docs/quickstart.md

Sending findings somewhere

Two push channels, both opt-in, both pointing at somewhere you own. (The third delivery channel, inline PR annotations, is automatic in GitHub Actions; see above.) Without one of these flags rsscan makes no network call at all.

# Slack
rsscan --slack-webhook https://hooks.slack.com/services/T000/B000/xxxx
# Any JSON receiver you control
rsscan --webhook https://hooks.example.com/rsscan

None of them ever carries a secret value. Each finding is transmitted as its credential type, severity, file, line and fingerprint. That is the same guarantee as --report. The Slack message says so in its own footer, so whoever reads the channel knows it is safe to leave there.

They fire only when there are findings. A notification on every clean build trains people to ignore the channel, which is how a real finding gets missed.

Delivery failure never changes the exit code. If Slack is unreachable, rsscan warns on stderr and the build result stands on the scan alone: a gate should block on secrets, not on a flaky notification endpoint.

The generic webhook posts:

{
"tool": "rsscan", "version": "0.2.0", "scanned": "origin/main...HEAD",
"repo": "acme/api", "ref": "feature/pay", "build_url": "https://github.com/...",
"findings_count": 2, "blocking_count": 2, "highest_severity": "CRITICAL",
"severity_counts": {"CRITICAL": 2},
"findings": [
{"type": "aws_access_key", "severity": "CRITICAL", "description": "AWS IAM Access Key",
"file": "src/config.py", "line": 3, "fingerprint": "sha256:1a5d44a2dca19669"}
],
"detected_at": "2026-08-04T15:00:00+00:00"
}

Counting who can publish your dependencies

rsscan --deps # auto-detects package-lock.json, then package.json
rsscan --deps path/to/package-lock.json # or point it at one

A self-replicating npm worm does not start with malicious code. It starts with a maintainer account: an infostealer takes the publish token out of somebody's .npmrc, and a patch version nobody reads gets published four steps before there is any artifact for a scanner to analyse.

--deps tells you how large that surface is for your own tree:

 Who can publish your dependencies
433 dependencies in package-lock.json
275 distinct publisher accounts can push code into them
126 on personal webmail (no SSO, no central revocation)
28 role or automation addresses

Each package is resolved to its maintainers list plus the _npmUser who actually published the version you would install. Counts are of distinct email addresses, which is a proxy for accounts and imperfect in both directions: one person with two addresses counts twice, two people sharing one count once.

It reads locally and queries only registry.npmjs.org. No account, no API key, no network call to RelayShield, and no telemetry. It prints integers and names nobody.

It always exits 0. There is no dependency count that constitutes a build failure, so this is a report and deliberately not a gate.

If a package cannot be resolved, that count is printed separately and is not folded into the totals. A package whose publishers we could not look up is not a package with no publishers, and collapsing those two into one number is how a tool ends up quietly reassuring you.

Sharing a finding with your security team

rsscan --report exposure.md

Writes a Markdown report you can attach to a ticket or forward by email. It contains no secret values, only fingerprints, so it is safe to share.

Optional: telling us your org uses rsscan

rsscan --org yourcompany.com

Off by default and entirely optional. When enabled it sends only your org domain, an anonymous per-machine id, the tool version, and how many findings there were by severity.

It never sends file paths, fingerprints, repository names, source code, or the secrets themselves. There is no mechanism in the tool to do so.

What this tool cannot tell you

rsscan stops credentials before they enter git history. It cannot see credentials that have already left: a key committed last year, or one leaked through a dependency, a published package or a container image, may already be indexed and scraped.

Answering that needs a view of what is public, plus the identity layer secret scanners do not cover at all: workforce credentials surfacing in infostealer logs and breach dumps, SIM-swap risk on staff accounts, and session/token exposure. That is what RelayShield does.

Pricing

rsscan is free. There is no paid tier of this tool, no scan quota, and no account.

Licence

MIT

About

Block commits that introduce API keys, tokens and other machine credentials. Runs entirely locally.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

rsscan

PyPI is the published version of record. Block commits and builds that introduce API keys, tokens and other machine credentials.

Detects 31 credential patterns: AWS IAM keys, GitHub PATs, Stripe secrets, Slack tokens, private keys, and LLM provider keys (OpenAI, Anthropic, Google, Groq, xAI, Replicate).

Free, and it runs entirely on your machine. No account, no API key, no network call. Your source code never leaves the host. Matching happens locally against patterns shipped inside the package.

The pre-commit hook is the point. It runs before the commit enters git history. A CI check only sees the secret after a push, and by then it is in history and has to be rotated even if you delete the commit. The CI integrations below are a backstop, not a substitute.

New in 0.2.0:rsscan --deps counts the accounts that can publish into your npm dependencies. See Counting who can publish your dependencies.

Install

pre-commit hook (recommended)

# .pre-commit-config.yamlrepos:
- repo: https://github.com/RelayShield/rsscanrev: v0.2.0hooks:
- id: rsscan
pre-commit install

That is the whole setup. Nothing to configure, nothing to sign up for.

GitHub Actions

- uses: actions/checkout@v4with:
fetch-depth: 0# required: the range needs history
- uses: RelayShield/rsscan@v0.2.0with:
fail-on: HIGH

Findings are annotated inline on the changed lines in the pull request's Files tab, so a developer sees them where the code is rather than in collapsed build output. Blocking findings (at or above fail-on) appear as errors; everything else appears as warnings.

This needs no token, no permissions: block and no GitHub App. Annotations are emitted as workflow commands, not through the Checks API. Turn them off with annotate: off if you only want the log.

Annotations never contain the secret value. Each one carries the credential type, its severity, and the fingerprint you would add to .relayshield-allowlist to suppress a false positive.

GitLab CI/CD

rsscan:
image: relayshield/rsscan:0.2.0variables:
GIT_DEPTH: 0# required: GitLab shallow-clones, and a shallow clone# yields an empty diff, so the job would pass having# scanned nothingRSSCAN_REV_RANGE: "origin/$CI_DEFAULT_BRANCH...HEAD"RSSCAN_FAIL_ON: HIGHscript: ["rsscan"]

CircleCI

orbs:
rsscan: relayshield/rsscan@0.1.0workflows:
main:
jobs:
- rsscan/scan

Docker: Bitbucket Pipelines, Tekton, Drone, Woodpecker, Harness, anything else

docker run --rm -v "$PWD:/workspace" \
-e RSSCAN_REV_RANGE=origin/main...HEAD \
relayshield/rsscan:0.2.0

Bitbucket Pipelines:

- step:
script:
- pipe: docker://relayshield/rsscan:0.2.0variables:
RSSCAN_REV_RANGE: "origin/main...HEAD"

Jenkins, Azure DevOps, or any shell

pip install rsscan
RSSCAN_REV_RANGE="origin/main...HEAD" rsscan

How it works

Scans the diff locally and fails on a finding at or above --fail-on (default HIGH).

Only added lines are scanned. Secrets already in your files are not re-flagged, so the tool does not become unbypassable noise on a repo with legacy findings.

Nothing is transmitted and nothing is printed. There is no scan endpoint to send code to. Matched values never appear in output either: findings carry a file, a line and a non-reversible fingerprint, so a scan is safe to run in CI without leaking the secret into build logs.

 rsscan: secrets detected in staged changes
CRITICAL AWS IAM Access Key
src/config.py:14
fingerprint sha256:1a5d44a2dca19669
Commit refused.
Remove the value and load it from a secrets manager or environment
variable instead. If it has already left this machine, rotate it.
False positive? Add the fingerprint to .relayshield-allowlist:
echo 'sha256:1a5d44a2dca19669' >> .relayshield-allowlist
To bypass entirely: git commit --no-verify

Configuration

Every flag has an environment variable equivalent, which is how the CI clients drive it.

FlagEnv varDefaultMeaning
--fail-onRSSCAN_FAIL_ONHIGHLowest severity that fails. LOW/MEDIUM/HIGH/CRITICAL.
--rev-rangeRSSCAN_REV_RANGE(staged)Scan a commit range instead of staged changes.
--allowlistRSSCAN_ALLOWLIST.relayshield-allowlistFingerprints to ignore.
--reportRSSCAN_REPORT(off)Write a shareable exposure report to this path.
--orgRSSCAN_ORG(off)Opt in to reporting adoption for your org domain.
--strict / --no-strictRSSCAN_STRICTsee belowFail when the scan cannot run.
--annotateRSSCAN_ANNOTATEautoInline PR annotations. auto enables them inside GitHub Actions only; github forces; off disables.
--slack-webhookRSSCAN_SLACK_WEBHOOK(off)POST a findings summary to your own Slack incoming webhook.
--webhookRSSCAN_WEBHOOK(off)POST findings as JSON to an endpoint you control.

Failure behaviour differs by mode, on purpose

Pre-commit fails open. If the scan genuinely cannot run (an unreadable diff, a broken git invocation), it warns on stderr and lets the commit through. A hook that wedges every commit gets uninstalled, and then it catches nothing.

CI fails closed. A gate that silently reports success when it could not actually run is worse than no gate. It manufactures false assurance. Override either way with --strict / --no-strict or RSSCAN_STRICT.

Allowlisting

The allowlist holds fingerprints, not secrets: a file containing the actual values would be the same mistake this tool exists to prevent.

# .relayshield-allowlist
sha256:1a5d44a2dca19669 # documented example key in docs/quickstart.md

Sending findings somewhere

Two push channels, both opt-in, both pointing at somewhere you own. (The third delivery channel, inline PR annotations, is automatic in GitHub Actions; see above.) Without one of these flags rsscan makes no network call at all.

# Slack
rsscan --slack-webhook https://hooks.slack.com/services/T000/B000/xxxx
# Any JSON receiver you control
rsscan --webhook https://hooks.example.com/rsscan

None of them ever carries a secret value. Each finding is transmitted as its credential type, severity, file, line and fingerprint. That is the same guarantee as --report. The Slack message says so in its own footer, so whoever reads the channel knows it is safe to leave there.

They fire only when there are findings. A notification on every clean build trains people to ignore the channel, which is how a real finding gets missed.

Delivery failure never changes the exit code. If Slack is unreachable, rsscan warns on stderr and the build result stands on the scan alone: a gate should block on secrets, not on a flaky notification endpoint.

The generic webhook posts:

{
"tool": "rsscan", "version": "0.2.0", "scanned": "origin/main...HEAD",
"repo": "acme/api", "ref": "feature/pay", "build_url": "https://github.com/...",
"findings_count": 2, "blocking_count": 2, "highest_severity": "CRITICAL",
"severity_counts": {"CRITICAL": 2},
"findings": [
{"type": "aws_access_key", "severity": "CRITICAL", "description": "AWS IAM Access Key",
"file": "src/config.py", "line": 3, "fingerprint": "sha256:1a5d44a2dca19669"}
],
"detected_at": "2026-08-04T15:00:00+00:00"
}

Counting who can publish your dependencies

rsscan --deps # auto-detects package-lock.json, then package.json
rsscan --deps path/to/package-lock.json # or point it at one

A self-replicating npm worm does not start with malicious code. It starts with a maintainer account: an infostealer takes the publish token out of somebody's .npmrc, and a patch version nobody reads gets published four steps before there is any artifact for a scanner to analyse.

--deps tells you how large that surface is for your own tree:

 Who can publish your dependencies
433 dependencies in package-lock.json
275 distinct publisher accounts can push code into them
126 on personal webmail (no SSO, no central revocation)
28 role or automation addresses

Each package is resolved to its maintainers list plus the _npmUser who actually published the version you would install. Counts are of distinct email addresses, which is a proxy for accounts and imperfect in both directions: one person with two addresses counts twice, two people sharing one count once.

It reads locally and queries only registry.npmjs.org. No account, no API key, no network call to RelayShield, and no telemetry. It prints integers and names nobody.

It always exits 0. There is no dependency count that constitutes a build failure, so this is a report and deliberately not a gate.

If a package cannot be resolved, that count is printed separately and is not folded into the totals. A package whose publishers we could not look up is not a package with no publishers, and collapsing those two into one number is how a tool ends up quietly reassuring you.

Sharing a finding with your security team

rsscan --report exposure.md

Writes a Markdown report you can attach to a ticket or forward by email. It contains no secret values, only fingerprints, so it is safe to share.

Optional: telling us your org uses rsscan

rsscan --org yourcompany.com

Off by default and entirely optional. When enabled it sends only your org domain, an anonymous per-machine id, the tool version, and how many findings there were by severity.

It never sends file paths, fingerprints, repository names, source code, or the secrets themselves. There is no mechanism in the tool to do so.

What this tool cannot tell you

rsscan stops credentials before they enter git history. It cannot see credentials that have already left: a key committed last year, or one leaked through a dependency, a published package or a container image, may already be indexed and scraped.

Answering that needs a view of what is public, plus the identity layer secret scanners do not cover at all: workforce credentials surfacing in infostealer logs and breach dumps, SIM-swap risk on staff accounts, and session/token exposure. That is what RelayShield does.

Pricing

rsscan is free. There is no paid tier of this tool, no scan quota, and no account.

Licence

MIT

About

Block commits that introduce API keys, tokens and other machine credentials. Runs entirely locally.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

rsscan

PyPI is the published version of record. Block commits and builds that introduce API keys, tokens and other machine credentials.

Detects 31 credential patterns: AWS IAM keys, GitHub PATs, Stripe secrets, Slack tokens, private keys, and LLM provider keys (OpenAI, Anthropic, Google, Groq, xAI, Replicate).

Free, and it runs entirely on your machine. No account, no API key, no network call. Your source code never leaves the host. Matching happens locally against patterns shipped inside the package.

The pre-commit hook is the point. It runs before the commit enters git history. A CI check only sees the secret after a push, and by then it is in history and has to be rotated even if you delete the commit. The CI integrations below are a backstop, not a substitute.

New in 0.2.0:rsscan --deps counts the accounts that can publish into your npm dependencies. See Counting who can publish your dependencies.

Install

pre-commit hook (recommended)

# .pre-commit-config.yamlrepos:
- repo: https://github.com/RelayShield/rsscanrev: v0.2.0hooks:
- id: rsscan
pre-commit install

That is the whole setup. Nothing to configure, nothing to sign up for.

GitHub Actions

- uses: actions/checkout@v4with:
fetch-depth: 0# required: the range needs history
- uses: RelayShield/rsscan@v0.2.0with:
fail-on: HIGH

Findings are annotated inline on the changed lines in the pull request's Files tab, so a developer sees them where the code is rather than in collapsed build output. Blocking findings (at or above fail-on) appear as errors; everything else appears as warnings.

This needs no token, no permissions: block and no GitHub App. Annotations are emitted as workflow commands, not through the Checks API. Turn them off with annotate: off if you only want the log.

Annotations never contain the secret value. Each one carries the credential type, its severity, and the fingerprint you would add to .relayshield-allowlist to suppress a false positive.

GitLab CI/CD

rsscan:
image: relayshield/rsscan:0.2.0variables:
GIT_DEPTH: 0# required: GitLab shallow-clones, and a shallow clone# yields an empty diff, so the job would pass having# scanned nothingRSSCAN_REV_RANGE: "origin/$CI_DEFAULT_BRANCH...HEAD"RSSCAN_FAIL_ON: HIGHscript: ["rsscan"]

CircleCI

orbs:
rsscan: relayshield/rsscan@0.1.0workflows:
main:
jobs:
- rsscan/scan

Docker: Bitbucket Pipelines, Tekton, Drone, Woodpecker, Harness, anything else

docker run --rm -v "$PWD:/workspace" \
-e RSSCAN_REV_RANGE=origin/main...HEAD \
relayshield/rsscan:0.2.0

Bitbucket Pipelines:

- step:
script:
- pipe: docker://relayshield/rsscan:0.2.0variables:
RSSCAN_REV_RANGE: "origin/main...HEAD"

Jenkins, Azure DevOps, or any shell

pip install rsscan
RSSCAN_REV_RANGE="origin/main...HEAD" rsscan

How it works

Scans the diff locally and fails on a finding at or above --fail-on (default HIGH).

Only added lines are scanned. Secrets already in your files are not re-flagged, so the tool does not become unbypassable noise on a repo with legacy findings.

Nothing is transmitted and nothing is printed. There is no scan endpoint to send code to. Matched values never appear in output either: findings carry a file, a line and a non-reversible fingerprint, so a scan is safe to run in CI without leaking the secret into build logs.

 rsscan: secrets detected in staged changes
CRITICAL AWS IAM Access Key
src/config.py:14
fingerprint sha256:1a5d44a2dca19669
Commit refused.
Remove the value and load it from a secrets manager or environment
variable instead. If it has already left this machine, rotate it.
False positive? Add the fingerprint to .relayshield-allowlist:
echo 'sha256:1a5d44a2dca19669' >> .relayshield-allowlist
To bypass entirely: git commit --no-verify

Configuration

Every flag has an environment variable equivalent, which is how the CI clients drive it.

FlagEnv varDefaultMeaning
--fail-onRSSCAN_FAIL_ONHIGHLowest severity that fails. LOW/MEDIUM/HIGH/CRITICAL.
--rev-rangeRSSCAN_REV_RANGE(staged)Scan a commit range instead of staged changes.
--allowlistRSSCAN_ALLOWLIST.relayshield-allowlistFingerprints to ignore.
--reportRSSCAN_REPORT(off)Write a shareable exposure report to this path.
--orgRSSCAN_ORG(off)Opt in to reporting adoption for your org domain.
--strict / --no-strictRSSCAN_STRICTsee belowFail when the scan cannot run.
--annotateRSSCAN_ANNOTATEautoInline PR annotations. auto enables them inside GitHub Actions only; github forces; off disables.
--slack-webhookRSSCAN_SLACK_WEBHOOK(off)POST a findings summary to your own Slack incoming webhook.
--webhookRSSCAN_WEBHOOK(off)POST findings as JSON to an endpoint you control.

Failure behaviour differs by mode, on purpose

Pre-commit fails open. If the scan genuinely cannot run (an unreadable diff, a broken git invocation), it warns on stderr and lets the commit through. A hook that wedges every commit gets uninstalled, and then it catches nothing.

CI fails closed. A gate that silently reports success when it could not actually run is worse than no gate. It manufactures false assurance. Override either way with --strict / --no-strict or RSSCAN_STRICT.

Allowlisting

The allowlist holds fingerprints, not secrets: a file containing the actual values would be the same mistake this tool exists to prevent.

# .relayshield-allowlist
sha256:1a5d44a2dca19669 # documented example key in docs/quickstart.md

Sending findings somewhere

Two push channels, both opt-in, both pointing at somewhere you own. (The third delivery channel, inline PR annotations, is automatic in GitHub Actions; see above.) Without one of these flags rsscan makes no network call at all.

# Slack
rsscan --slack-webhook https://hooks.slack.com/services/T000/B000/xxxx
# Any JSON receiver you control
rsscan --webhook https://hooks.example.com/rsscan

None of them ever carries a secret value. Each finding is transmitted as its credential type, severity, file, line and fingerprint. That is the same guarantee as --report. The Slack message says so in its own footer, so whoever reads the channel knows it is safe to leave there.

They fire only when there are findings. A notification on every clean build trains people to ignore the channel, which is how a real finding gets missed.

Delivery failure never changes the exit code. If Slack is unreachable, rsscan warns on stderr and the build result stands on the scan alone: a gate should block on secrets, not on a flaky notification endpoint.

The generic webhook posts:

{
"tool": "rsscan", "version": "0.2.0", "scanned": "origin/main...HEAD",
"repo": "acme/api", "ref": "feature/pay", "build_url": "https://github.com/...",
"findings_count": 2, "blocking_count": 2, "highest_severity": "CRITICAL",
"severity_counts": {"CRITICAL": 2},
"findings": [
{"type": "aws_access_key", "severity": "CRITICAL", "description": "AWS IAM Access Key",
"file": "src/config.py", "line": 3, "fingerprint": "sha256:1a5d44a2dca19669"}
],
"detected_at": "2026-08-04T15:00:00+00:00"
}

Counting who can publish your dependencies

rsscan --deps # auto-detects package-lock.json, then package.json
rsscan --deps path/to/package-lock.json # or point it at one

A self-replicating npm worm does not start with malicious code. It starts with a maintainer account: an infostealer takes the publish token out of somebody's .npmrc, and a patch version nobody reads gets published four steps before there is any artifact for a scanner to analyse.

--deps tells you how large that surface is for your own tree:

 Who can publish your dependencies
433 dependencies in package-lock.json
275 distinct publisher accounts can push code into them
126 on personal webmail (no SSO, no central revocation)
28 role or automation addresses

Each package is resolved to its maintainers list plus the _npmUser who actually published the version you would install. Counts are of distinct email addresses, which is a proxy for accounts and imperfect in both directions: one person with two addresses counts twice, two people sharing one count once.

It reads locally and queries only registry.npmjs.org. No account, no API key, no network call to RelayShield, and no telemetry. It prints integers and names nobody.

It always exits 0. There is no dependency count that constitutes a build failure, so this is a report and deliberately not a gate.

If a package cannot be resolved, that count is printed separately and is not folded into the totals. A package whose publishers we could not look up is not a package with no publishers, and collapsing those two into one number is how a tool ends up quietly reassuring you.

Sharing a finding with your security team

rsscan --report exposure.md

Writes a Markdown report you can attach to a ticket or forward by email. It contains no secret values, only fingerprints, so it is safe to share.

Optional: telling us your org uses rsscan

rsscan --org yourcompany.com

Off by default and entirely optional. When enabled it sends only your org domain, an anonymous per-machine id, the tool version, and how many findings there were by severity.

It never sends file paths, fingerprints, repository names, source code, or the secrets themselves. There is no mechanism in the tool to do so.

What this tool cannot tell you

rsscan stops credentials before they enter git history. It cannot see credentials that have already left: a key committed last year, or one leaked through a dependency, a published package or a container image, may already be indexed and scraped.

Answering that needs a view of what is public, plus the identity layer secret scanners do not cover at all: workforce credentials surfacing in infostealer logs and breach dumps, SIM-swap risk on staff accounts, and session/token exposure. That is what RelayShield does.

Pricing

rsscan is free. There is no paid tier of this tool, no scan quota, and no account.

Licence

MIT

About

Block commits that introduce API keys, tokens and other machine credentials. Runs entirely locally.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

rsscan

PyPI is the published version of record. Block commits and builds that introduce API keys, tokens and other machine credentials.

Detects 31 credential patterns: AWS IAM keys, GitHub PATs, Stripe secrets, Slack tokens, private keys, and LLM provider keys (OpenAI, Anthropic, Google, Groq, xAI, Replicate).

Free, and it runs entirely on your machine. No account, no API key, no network call. Your source code never leaves the host. Matching happens locally against patterns shipped inside the package.

The pre-commit hook is the point. It runs before the commit enters git history. A CI check only sees the secret after a push, and by then it is in history and has to be rotated even if you delete the commit. The CI integrations below are a backstop, not a substitute.

New in 0.2.0:rsscan --deps counts the accounts that can publish into your npm dependencies. See Counting who can publish your dependencies.

Install

pre-commit hook (recommended)

# .pre-commit-config.yamlrepos:
- repo: https://github.com/RelayShield/rsscanrev: v0.2.0hooks:
- id: rsscan
pre-commit install

That is the whole setup. Nothing to configure, nothing to sign up for.

GitHub Actions

- uses: actions/checkout@v4with:
fetch-depth: 0# required: the range needs history
- uses: RelayShield/rsscan@v0.2.0with:
fail-on: HIGH

Findings are annotated inline on the changed lines in the pull request's Files tab, so a developer sees them where the code is rather than in collapsed build output. Blocking findings (at or above fail-on) appear as errors; everything else appears as warnings.

This needs no token, no permissions: block and no GitHub App. Annotations are emitted as workflow commands, not through the Checks API. Turn them off with annotate: off if you only want the log.

Annotations never contain the secret value. Each one carries the credential type, its severity, and the fingerprint you would add to .relayshield-allowlist to suppress a false positive.

GitLab CI/CD

rsscan:
image: relayshield/rsscan:0.2.0variables:
GIT_DEPTH: 0# required: GitLab shallow-clones, and a shallow clone# yields an empty diff, so the job would pass having# scanned nothingRSSCAN_REV_RANGE: "origin/$CI_DEFAULT_BRANCH...HEAD"RSSCAN_FAIL_ON: HIGHscript: ["rsscan"]

CircleCI

orbs:
rsscan: relayshield/rsscan@0.1.0workflows:
main:
jobs:
- rsscan/scan

Docker: Bitbucket Pipelines, Tekton, Drone, Woodpecker, Harness, anything else

docker run --rm -v "$PWD:/workspace" \
-e RSSCAN_REV_RANGE=origin/main...HEAD \
relayshield/rsscan:0.2.0

Bitbucket Pipelines:

- step:
script:
- pipe: docker://relayshield/rsscan:0.2.0variables:
RSSCAN_REV_RANGE: "origin/main...HEAD"

Jenkins, Azure DevOps, or any shell

pip install rsscan
RSSCAN_REV_RANGE="origin/main...HEAD" rsscan

How it works

Scans the diff locally and fails on a finding at or above --fail-on (default HIGH).

Only added lines are scanned. Secrets already in your files are not re-flagged, so the tool does not become unbypassable noise on a repo with legacy findings.

Nothing is transmitted and nothing is printed. There is no scan endpoint to send code to. Matched values never appear in output either: findings carry a file, a line and a non-reversible fingerprint, so a scan is safe to run in CI without leaking the secret into build logs.

 rsscan: secrets detected in staged changes
CRITICAL AWS IAM Access Key
src/config.py:14
fingerprint sha256:1a5d44a2dca19669
Commit refused.
Remove the value and load it from a secrets manager or environment
variable instead. If it has already left this machine, rotate it.
False positive? Add the fingerprint to .relayshield-allowlist:
echo 'sha256:1a5d44a2dca19669' >> .relayshield-allowlist
To bypass entirely: git commit --no-verify

Configuration

Every flag has an environment variable equivalent, which is how the CI clients drive it.

FlagEnv varDefaultMeaning
--fail-onRSSCAN_FAIL_ONHIGHLowest severity that fails. LOW/MEDIUM/HIGH/CRITICAL.
--rev-rangeRSSCAN_REV_RANGE(staged)Scan a commit range instead of staged changes.
--allowlistRSSCAN_ALLOWLIST.relayshield-allowlistFingerprints to ignore.
--reportRSSCAN_REPORT(off)Write a shareable exposure report to this path.
--orgRSSCAN_ORG(off)Opt in to reporting adoption for your org domain.
--strict / --no-strictRSSCAN_STRICTsee belowFail when the scan cannot run.
--annotateRSSCAN_ANNOTATEautoInline PR annotations. auto enables them inside GitHub Actions only; github forces; off disables.
--slack-webhookRSSCAN_SLACK_WEBHOOK(off)POST a findings summary to your own Slack incoming webhook.
--webhookRSSCAN_WEBHOOK(off)POST findings as JSON to an endpoint you control.

Failure behaviour differs by mode, on purpose

Pre-commit fails open. If the scan genuinely cannot run (an unreadable diff, a broken git invocation), it warns on stderr and lets the commit through. A hook that wedges every commit gets uninstalled, and then it catches nothing.

CI fails closed. A gate that silently reports success when it could not actually run is worse than no gate. It manufactures false assurance. Override either way with --strict / --no-strict or RSSCAN_STRICT.

Allowlisting

The allowlist holds fingerprints, not secrets: a file containing the actual values would be the same mistake this tool exists to prevent.

# .relayshield-allowlist
sha256:1a5d44a2dca19669 # documented example key in docs/quickstart.md

Sending findings somewhere

Two push channels, both opt-in, both pointing at somewhere you own. (The third delivery channel, inline PR annotations, is automatic in GitHub Actions; see above.) Without one of these flags rsscan makes no network call at all.

# Slack
rsscan --slack-webhook https://hooks.slack.com/services/T000/B000/xxxx
# Any JSON receiver you control
rsscan --webhook https://hooks.example.com/rsscan

None of them ever carries a secret value. Each finding is transmitted as its credential type, severity, file, line and fingerprint. That is the same guarantee as --report. The Slack message says so in its own footer, so whoever reads the channel knows it is safe to leave there.

They fire only when there are findings. A notification on every clean build trains people to ignore the channel, which is how a real finding gets missed.

Delivery failure never changes the exit code. If Slack is unreachable, rsscan warns on stderr and the build result stands on the scan alone: a gate should block on secrets, not on a flaky notification endpoint.

The generic webhook posts:

{
"tool": "rsscan", "version": "0.2.0", "scanned": "origin/main...HEAD",
"repo": "acme/api", "ref": "feature/pay", "build_url": "https://github.com/...",
"findings_count": 2, "blocking_count": 2, "highest_severity": "CRITICAL",
"severity_counts": {"CRITICAL": 2},
"findings": [
{"type": "aws_access_key", "severity": "CRITICAL", "description": "AWS IAM Access Key",
"file": "src/config.py", "line": 3, "fingerprint": "sha256:1a5d44a2dca19669"}
],
"detected_at": "2026-08-04T15:00:00+00:00"
}

Counting who can publish your dependencies

rsscan --deps # auto-detects package-lock.json, then package.json
rsscan --deps path/to/package-lock.json # or point it at one

A self-replicating npm worm does not start with malicious code. It starts with a maintainer account: an infostealer takes the publish token out of somebody's .npmrc, and a patch version nobody reads gets published four steps before there is any artifact for a scanner to analyse.

--deps tells you how large that surface is for your own tree:

 Who can publish your dependencies
433 dependencies in package-lock.json
275 distinct publisher accounts can push code into them
126 on personal webmail (no SSO, no central revocation)
28 role or automation addresses

Each package is resolved to its maintainers list plus the _npmUser who actually published the version you would install. Counts are of distinct email addresses, which is a proxy for accounts and imperfect in both directions: one person with two addresses counts twice, two people sharing one count once.

It reads locally and queries only registry.npmjs.org. No account, no API key, no network call to RelayShield, and no telemetry. It prints integers and names nobody.

It always exits 0. There is no dependency count that constitutes a build failure, so this is a report and deliberately not a gate.

If a package cannot be resolved, that count is printed separately and is not folded into the totals. A package whose publishers we could not look up is not a package with no publishers, and collapsing those two into one number is how a tool ends up quietly reassuring you.

Sharing a finding with your security team

rsscan --report exposure.md

Writes a Markdown report you can attach to a ticket or forward by email. It contains no secret values, only fingerprints, so it is safe to share.

Optional: telling us your org uses rsscan

rsscan --org yourcompany.com

Off by default and entirely optional. When enabled it sends only your org domain, an anonymous per-machine id, the tool version, and how many findings there were by severity.

It never sends file paths, fingerprints, repository names, source code, or the secrets themselves. There is no mechanism in the tool to do so.

What this tool cannot tell you

rsscan stops credentials before they enter git history. It cannot see credentials that have already left: a key committed last year, or one leaked through a dependency, a published package or a container image, may already be indexed and scraped.

Answering that needs a view of what is public, plus the identity layer secret scanners do not cover at all: workforce credentials surfacing in infostealer logs and breach dumps, SIM-swap risk on staff accounts, and session/token exposure. That is what RelayShield does.

Pricing

rsscan is free. There is no paid tier of this tool, no scan quota, and no account.

Licence

MIT

About

Block commits that introduce API keys, tokens and other machine credentials. Runs entirely locally.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

rsscan

PyPI is the published version of record. Block commits and builds that introduce API keys, tokens and other machine credentials.

Detects 31 credential patterns: AWS IAM keys, GitHub PATs, Stripe secrets, Slack tokens, private keys, and LLM provider keys (OpenAI, Anthropic, Google, Groq, xAI, Replicate).

Free, and it runs entirely on your machine. No account, no API key, no network call. Your source code never leaves the host. Matching happens locally against patterns shipped inside the package.

The pre-commit hook is the point. It runs before the commit enters git history. A CI check only sees the secret after a push, and by then it is in history and has to be rotated even if you delete the commit. The CI integrations below are a backstop, not a substitute.

New in 0.2.0:rsscan --deps counts the accounts that can publish into your npm dependencies. See Counting who can publish your dependencies.

Install

pre-commit hook (recommended)

# .pre-commit-config.yamlrepos:
- repo: https://github.com/RelayShield/rsscanrev: v0.2.0hooks:
- id: rsscan
pre-commit install

That is the whole setup. Nothing to configure, nothing to sign up for.

GitHub Actions

- uses: actions/checkout@v4with:
fetch-depth: 0# required: the range needs history
- uses: RelayShield/rsscan@v0.2.0with:
fail-on: HIGH

Findings are annotated inline on the changed lines in the pull request's Files tab, so a developer sees them where the code is rather than in collapsed build output. Blocking findings (at or above fail-on) appear as errors; everything else appears as warnings.

This needs no token, no permissions: block and no GitHub App. Annotations are emitted as workflow commands, not through the Checks API. Turn them off with annotate: off if you only want the log.

Annotations never contain the secret value. Each one carries the credential type, its severity, and the fingerprint you would add to .relayshield-allowlist to suppress a false positive.

GitLab CI/CD

rsscan:
image: relayshield/rsscan:0.2.0variables:
GIT_DEPTH: 0# required: GitLab shallow-clones, and a shallow clone# yields an empty diff, so the job would pass having# scanned nothingRSSCAN_REV_RANGE: "origin/$CI_DEFAULT_BRANCH...HEAD"RSSCAN_FAIL_ON: HIGHscript: ["rsscan"]

CircleCI

orbs:
rsscan: relayshield/rsscan@0.1.0workflows:
main:
jobs:
- rsscan/scan

Docker: Bitbucket Pipelines, Tekton, Drone, Woodpecker, Harness, anything else

docker run --rm -v "$PWD:/workspace" \
-e RSSCAN_REV_RANGE=origin/main...HEAD \
relayshield/rsscan:0.2.0

Bitbucket Pipelines:

- step:
script:
- pipe: docker://relayshield/rsscan:0.2.0variables:
RSSCAN_REV_RANGE: "origin/main...HEAD"

Jenkins, Azure DevOps, or any shell

pip install rsscan
RSSCAN_REV_RANGE="origin/main...HEAD" rsscan

How it works

Scans the diff locally and fails on a finding at or above --fail-on (default HIGH).

Only added lines are scanned. Secrets already in your files are not re-flagged, so the tool does not become unbypassable noise on a repo with legacy findings.

Nothing is transmitted and nothing is printed. There is no scan endpoint to send code to. Matched values never appear in output either: findings carry a file, a line and a non-reversible fingerprint, so a scan is safe to run in CI without leaking the secret into build logs.

 rsscan: secrets detected in staged changes
CRITICAL AWS IAM Access Key
src/config.py:14
fingerprint sha256:1a5d44a2dca19669
Commit refused.
Remove the value and load it from a secrets manager or environment
variable instead. If it has already left this machine, rotate it.
False positive? Add the fingerprint to .relayshield-allowlist:
echo 'sha256:1a5d44a2dca19669' >> .relayshield-allowlist
To bypass entirely: git commit --no-verify

Configuration

Every flag has an environment variable equivalent, which is how the CI clients drive it.

FlagEnv varDefaultMeaning
--fail-onRSSCAN_FAIL_ONHIGHLowest severity that fails. LOW/MEDIUM/HIGH/CRITICAL.
--rev-rangeRSSCAN_REV_RANGE(staged)Scan a commit range instead of staged changes.
--allowlistRSSCAN_ALLOWLIST.relayshield-allowlistFingerprints to ignore.
--reportRSSCAN_REPORT(off)Write a shareable exposure report to this path.
--orgRSSCAN_ORG(off)Opt in to reporting adoption for your org domain.
--strict / --no-strictRSSCAN_STRICTsee belowFail when the scan cannot run.
--annotateRSSCAN_ANNOTATEautoInline PR annotations. auto enables them inside GitHub Actions only; github forces; off disables.
--slack-webhookRSSCAN_SLACK_WEBHOOK(off)POST a findings summary to your own Slack incoming webhook.
--webhookRSSCAN_WEBHOOK(off)POST findings as JSON to an endpoint you control.

Failure behaviour differs by mode, on purpose

Pre-commit fails open. If the scan genuinely cannot run (an unreadable diff, a broken git invocation), it warns on stderr and lets the commit through. A hook that wedges every commit gets uninstalled, and then it catches nothing.

CI fails closed. A gate that silently reports success when it could not actually run is worse than no gate. It manufactures false assurance. Override either way with --strict / --no-strict or RSSCAN_STRICT.

Allowlisting

The allowlist holds fingerprints, not secrets: a file containing the actual values would be the same mistake this tool exists to prevent.

# .relayshield-allowlist
sha256:1a5d44a2dca19669 # documented example key in docs/quickstart.md

Sending findings somewhere

Two push channels, both opt-in, both pointing at somewhere you own. (The third delivery channel, inline PR annotations, is automatic in GitHub Actions; see above.) Without one of these flags rsscan makes no network call at all.

# Slack
rsscan --slack-webhook https://hooks.slack.com/services/T000/B000/xxxx
# Any JSON receiver you control
rsscan --webhook https://hooks.example.com/rsscan

None of them ever carries a secret value. Each finding is transmitted as its credential type, severity, file, line and fingerprint. That is the same guarantee as --report. The Slack message says so in its own footer, so whoever reads the channel knows it is safe to leave there.

They fire only when there are findings. A notification on every clean build trains people to ignore the channel, which is how a real finding gets missed.

Delivery failure never changes the exit code. If Slack is unreachable, rsscan warns on stderr and the build result stands on the scan alone: a gate should block on secrets, not on a flaky notification endpoint.

The generic webhook posts:

{
"tool": "rsscan", "version": "0.2.0", "scanned": "origin/main...HEAD",
"repo": "acme/api", "ref": "feature/pay", "build_url": "https://github.com/...",
"findings_count": 2, "blocking_count": 2, "highest_severity": "CRITICAL",
"severity_counts": {"CRITICAL": 2},
"findings": [
{"type": "aws_access_key", "severity": "CRITICAL", "description": "AWS IAM Access Key",
"file": "src/config.py", "line": 3, "fingerprint": "sha256:1a5d44a2dca19669"}
],
"detected_at": "2026-08-04T15:00:00+00:00"
}

Counting who can publish your dependencies

rsscan --deps # auto-detects package-lock.json, then package.json
rsscan --deps path/to/package-lock.json # or point it at one

A self-replicating npm worm does not start with malicious code. It starts with a maintainer account: an infostealer takes the publish token out of somebody's .npmrc, and a patch version nobody reads gets published four steps before there is any artifact for a scanner to analyse.

--deps tells you how large that surface is for your own tree:

 Who can publish your dependencies
433 dependencies in package-lock.json
275 distinct publisher accounts can push code into them
126 on personal webmail (no SSO, no central revocation)
28 role or automation addresses

Each package is resolved to its maintainers list plus the _npmUser who actually published the version you would install. Counts are of distinct email addresses, which is a proxy for accounts and imperfect in both directions: one person with two addresses counts twice, two people sharing one count once.

It reads locally and queries only registry.npmjs.org. No account, no API key, no network call to RelayShield, and no telemetry. It prints integers and names nobody.

It always exits 0. There is no dependency count that constitutes a build failure, so this is a report and deliberately not a gate.

If a package cannot be resolved, that count is printed separately and is not folded into the totals. A package whose publishers we could not look up is not a package with no publishers, and collapsing those two into one number is how a tool ends up quietly reassuring you.

Sharing a finding with your security team

rsscan --report exposure.md

Writes a Markdown report you can attach to a ticket or forward by email. It contains no secret values, only fingerprints, so it is safe to share.

Optional: telling us your org uses rsscan

rsscan --org yourcompany.com

Off by default and entirely optional. When enabled it sends only your org domain, an anonymous per-machine id, the tool version, and how many findings there were by severity.

It never sends file paths, fingerprints, repository names, source code, or the secrets themselves. There is no mechanism in the tool to do so.

What this tool cannot tell you

rsscan stops credentials before they enter git history. It cannot see credentials that have already left: a key committed last year, or one leaked through a dependency, a published package or a container image, may already be indexed and scraped.

Answering that needs a view of what is public, plus the identity layer secret scanners do not cover at all: workforce credentials surfacing in infostealer logs and breach dumps, SIM-swap risk on staff accounts, and session/token exposure. That is what RelayShield does.

Pricing

rsscan is free. There is no paid tier of this tool, no scan quota, and no account.

Licence

MIT

About

Block commits that introduce API keys, tokens and other machine credentials. Runs entirely locally.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

rsscan

PyPI is the published version of record. Block commits and builds that introduce API keys, tokens and other machine credentials.

Detects 31 credential patterns: AWS IAM keys, GitHub PATs, Stripe secrets, Slack tokens, private keys, and LLM provider keys (OpenAI, Anthropic, Google, Groq, xAI, Replicate).

Free, and it runs entirely on your machine. No account, no API key, no network call. Your source code never leaves the host. Matching happens locally against patterns shipped inside the package.

The pre-commit hook is the point. It runs before the commit enters git history. A CI check only sees the secret after a push, and by then it is in history and has to be rotated even if you delete the commit. The CI integrations below are a backstop, not a substitute.

New in 0.2.0:rsscan --deps counts the accounts that can publish into your npm dependencies. See Counting who can publish your dependencies.

Install

pre-commit hook (recommended)

# .pre-commit-config.yamlrepos:
- repo: https://github.com/RelayShield/rsscanrev: v0.2.0hooks:
- id: rsscan
pre-commit install

That is the whole setup. Nothing to configure, nothing to sign up for.

GitHub Actions

- uses: actions/checkout@v4with:
fetch-depth: 0# required: the range needs history
- uses: RelayShield/rsscan@v0.2.0with:
fail-on: HIGH

Findings are annotated inline on the changed lines in the pull request's Files tab, so a developer sees them where the code is rather than in collapsed build output. Blocking findings (at or above fail-on) appear as errors; everything else appears as warnings.

This needs no token, no permissions: block and no GitHub App. Annotations are emitted as workflow commands, not through the Checks API. Turn them off with annotate: off if you only want the log.

Annotations never contain the secret value. Each one carries the credential type, its severity, and the fingerprint you would add to .relayshield-allowlist to suppress a false positive.

GitLab CI/CD

rsscan:
image: relayshield/rsscan:0.2.0variables:
GIT_DEPTH: 0# required: GitLab shallow-clones, and a shallow clone# yields an empty diff, so the job would pass having# scanned nothingRSSCAN_REV_RANGE: "origin/$CI_DEFAULT_BRANCH...HEAD"RSSCAN_FAIL_ON: HIGHscript: ["rsscan"]

CircleCI

orbs:
rsscan: relayshield/rsscan@0.1.0workflows:
main:
jobs:
- rsscan/scan

Docker: Bitbucket Pipelines, Tekton, Drone, Woodpecker, Harness, anything else

docker run --rm -v "$PWD:/workspace" \
-e RSSCAN_REV_RANGE=origin/main...HEAD \
relayshield/rsscan:0.2.0

Bitbucket Pipelines:

- step:
script:
- pipe: docker://relayshield/rsscan:0.2.0variables:
RSSCAN_REV_RANGE: "origin/main...HEAD"

Jenkins, Azure DevOps, or any shell

pip install rsscan
RSSCAN_REV_RANGE="origin/main...HEAD" rsscan

How it works

Scans the diff locally and fails on a finding at or above --fail-on (default HIGH).

Only added lines are scanned. Secrets already in your files are not re-flagged, so the tool does not become unbypassable noise on a repo with legacy findings.

Nothing is transmitted and nothing is printed. There is no scan endpoint to send code to. Matched values never appear in output either: findings carry a file, a line and a non-reversible fingerprint, so a scan is safe to run in CI without leaking the secret into build logs.

 rsscan: secrets detected in staged changes
CRITICAL AWS IAM Access Key
src/config.py:14
fingerprint sha256:1a5d44a2dca19669
Commit refused.
Remove the value and load it from a secrets manager or environment
variable instead. If it has already left this machine, rotate it.
False positive? Add the fingerprint to .relayshield-allowlist:
echo 'sha256:1a5d44a2dca19669' >> .relayshield-allowlist
To bypass entirely: git commit --no-verify

Configuration

Every flag has an environment variable equivalent, which is how the CI clients drive it.

FlagEnv varDefaultMeaning
--fail-onRSSCAN_FAIL_ONHIGHLowest severity that fails. LOW/MEDIUM/HIGH/CRITICAL.
--rev-rangeRSSCAN_REV_RANGE(staged)Scan a commit range instead of staged changes.
--allowlistRSSCAN_ALLOWLIST.relayshield-allowlistFingerprints to ignore.
--reportRSSCAN_REPORT(off)Write a shareable exposure report to this path.
--orgRSSCAN_ORG(off)Opt in to reporting adoption for your org domain.
--strict / --no-strictRSSCAN_STRICTsee belowFail when the scan cannot run.
--annotateRSSCAN_ANNOTATEautoInline PR annotations. auto enables them inside GitHub Actions only; github forces; off disables.
--slack-webhookRSSCAN_SLACK_WEBHOOK(off)POST a findings summary to your own Slack incoming webhook.
--webhookRSSCAN_WEBHOOK(off)POST findings as JSON to an endpoint you control.

Failure behaviour differs by mode, on purpose

Pre-commit fails open. If the scan genuinely cannot run (an unreadable diff, a broken git invocation), it warns on stderr and lets the commit through. A hook that wedges every commit gets uninstalled, and then it catches nothing.

CI fails closed. A gate that silently reports success when it could not actually run is worse than no gate. It manufactures false assurance. Override either way with --strict / --no-strict or RSSCAN_STRICT.

Allowlisting

The allowlist holds fingerprints, not secrets: a file containing the actual values would be the same mistake this tool exists to prevent.

# .relayshield-allowlist
sha256:1a5d44a2dca19669 # documented example key in docs/quickstart.md

Sending findings somewhere

Two push channels, both opt-in, both pointing at somewhere you own. (The third delivery channel, inline PR annotations, is automatic in GitHub Actions; see above.) Without one of these flags rsscan makes no network call at all.

# Slack
rsscan --slack-webhook https://hooks.slack.com/services/T000/B000/xxxx
# Any JSON receiver you control
rsscan --webhook https://hooks.example.com/rsscan

None of them ever carries a secret value. Each finding is transmitted as its credential type, severity, file, line and fingerprint. That is the same guarantee as --report. The Slack message says so in its own footer, so whoever reads the channel knows it is safe to leave there.

They fire only when there are findings. A notification on every clean build trains people to ignore the channel, which is how a real finding gets missed.

Delivery failure never changes the exit code. If Slack is unreachable, rsscan warns on stderr and the build result stands on the scan alone: a gate should block on secrets, not on a flaky notification endpoint.

The generic webhook posts:

{
"tool": "rsscan", "version": "0.2.0", "scanned": "origin/main...HEAD",
"repo": "acme/api", "ref": "feature/pay", "build_url": "https://github.com/...",
"findings_count": 2, "blocking_count": 2, "highest_severity": "CRITICAL",
"severity_counts": {"CRITICAL": 2},
"findings": [
{"type": "aws_access_key", "severity": "CRITICAL", "description": "AWS IAM Access Key",
"file": "src/config.py", "line": 3, "fingerprint": "sha256:1a5d44a2dca19669"}
],
"detected_at": "2026-08-04T15:00:00+00:00"
}

Counting who can publish your dependencies

rsscan --deps # auto-detects package-lock.json, then package.json
rsscan --deps path/to/package-lock.json # or point it at one

A self-replicating npm worm does not start with malicious code. It starts with a maintainer account: an infostealer takes the publish token out of somebody's .npmrc, and a patch version nobody reads gets published four steps before there is any artifact for a scanner to analyse.

--deps tells you how large that surface is for your own tree:

 Who can publish your dependencies
433 dependencies in package-lock.json
275 distinct publisher accounts can push code into them
126 on personal webmail (no SSO, no central revocation)
28 role or automation addresses

Each package is resolved to its maintainers list plus the _npmUser who actually published the version you would install. Counts are of distinct email addresses, which is a proxy for accounts and imperfect in both directions: one person with two addresses counts twice, two people sharing one count once.

It reads locally and queries only registry.npmjs.org. No account, no API key, no network call to RelayShield, and no telemetry. It prints integers and names nobody.

It always exits 0. There is no dependency count that constitutes a build failure, so this is a report and deliberately not a gate.

If a package cannot be resolved, that count is printed separately and is not folded into the totals. A package whose publishers we could not look up is not a package with no publishers, and collapsing those two into one number is how a tool ends up quietly reassuring you.

Sharing a finding with your security team

rsscan --report exposure.md

Writes a Markdown report you can attach to a ticket or forward by email. It contains no secret values, only fingerprints, so it is safe to share.

Optional: telling us your org uses rsscan

rsscan --org yourcompany.com

Off by default and entirely optional. When enabled it sends only your org domain, an anonymous per-machine id, the tool version, and how many findings there were by severity.

It never sends file paths, fingerprints, repository names, source code, or the secrets themselves. There is no mechanism in the tool to do so.

What this tool cannot tell you

rsscan stops credentials before they enter git history. It cannot see credentials that have already left: a key committed last year, or one leaked through a dependency, a published package or a container image, may already be indexed and scraped.

Answering that needs a view of what is public, plus the identity layer secret scanners do not cover at all: workforce credentials surfacing in infostealer logs and breach dumps, SIM-swap risk on staff accounts, and session/token exposure. That is what RelayShield does.

Pricing

rsscan is free. There is no paid tier of this tool, no scan quota, and no account.

Licence

MIT

About

Block commits that introduce API keys, tokens and other machine credentials. Runs entirely locally.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

rsscan

PyPI is the published version of record. Block commits and builds that introduce API keys, tokens and other machine credentials.

Detects 31 credential patterns: AWS IAM keys, GitHub PATs, Stripe secrets, Slack tokens, private keys, and LLM provider keys (OpenAI, Anthropic, Google, Groq, xAI, Replicate).

Free, and it runs entirely on your machine. No account, no API key, no network call. Your source code never leaves the host. Matching happens locally against patterns shipped inside the package.

The pre-commit hook is the point. It runs before the commit enters git history. A CI check only sees the secret after a push, and by then it is in history and has to be rotated even if you delete the commit. The CI integrations below are a backstop, not a substitute.

New in 0.2.0:rsscan --deps counts the accounts that can publish into your npm dependencies. See Counting who can publish your dependencies.

Install

pre-commit hook (recommended)

# .pre-commit-config.yamlrepos:
- repo: https://github.com/RelayShield/rsscanrev: v0.2.0hooks:
- id: rsscan
pre-commit install

That is the whole setup. Nothing to configure, nothing to sign up for.

GitHub Actions

- uses: actions/checkout@v4with:
fetch-depth: 0# required: the range needs history
- uses: RelayShield/rsscan@v0.2.0with:
fail-on: HIGH

Findings are annotated inline on the changed lines in the pull request's Files tab, so a developer sees them where the code is rather than in collapsed build output. Blocking findings (at or above fail-on) appear as errors; everything else appears as warnings.

This needs no token, no permissions: block and no GitHub App. Annotations are emitted as workflow commands, not through the Checks API. Turn them off with annotate: off if you only want the log.

Annotations never contain the secret value. Each one carries the credential type, its severity, and the fingerprint you would add to .relayshield-allowlist to suppress a false positive.

GitLab CI/CD

rsscan:
image: relayshield/rsscan:0.2.0variables:
GIT_DEPTH: 0# required: GitLab shallow-clones, and a shallow clone# yields an empty diff, so the job would pass having# scanned nothingRSSCAN_REV_RANGE: "origin/$CI_DEFAULT_BRANCH...HEAD"RSSCAN_FAIL_ON: HIGHscript: ["rsscan"]

CircleCI

orbs:
rsscan: relayshield/rsscan@0.1.0workflows:
main:
jobs:
- rsscan/scan

Docker: Bitbucket Pipelines, Tekton, Drone, Woodpecker, Harness, anything else

docker run --rm -v "$PWD:/workspace" \
-e RSSCAN_REV_RANGE=origin/main...HEAD \
relayshield/rsscan:0.2.0

Bitbucket Pipelines:

- step:
script:
- pipe: docker://relayshield/rsscan:0.2.0variables:
RSSCAN_REV_RANGE: "origin/main...HEAD"

Jenkins, Azure DevOps, or any shell

pip install rsscan
RSSCAN_REV_RANGE="origin/main...HEAD" rsscan

How it works

Scans the diff locally and fails on a finding at or above --fail-on (default HIGH).

Only added lines are scanned. Secrets already in your files are not re-flagged, so the tool does not become unbypassable noise on a repo with legacy findings.

Nothing is transmitted and nothing is printed. There is no scan endpoint to send code to. Matched values never appear in output either: findings carry a file, a line and a non-reversible fingerprint, so a scan is safe to run in CI without leaking the secret into build logs.

 rsscan: secrets detected in staged changes
CRITICAL AWS IAM Access Key
src/config.py:14
fingerprint sha256:1a5d44a2dca19669
Commit refused.
Remove the value and load it from a secrets manager or environment
variable instead. If it has already left this machine, rotate it.
False positive? Add the fingerprint to .relayshield-allowlist:
echo 'sha256:1a5d44a2dca19669' >> .relayshield-allowlist
To bypass entirely: git commit --no-verify

Configuration

Every flag has an environment variable equivalent, which is how the CI clients drive it.

FlagEnv varDefaultMeaning
--fail-onRSSCAN_FAIL_ONHIGHLowest severity that fails. LOW/MEDIUM/HIGH/CRITICAL.
--rev-rangeRSSCAN_REV_RANGE(staged)Scan a commit range instead of staged changes.
--allowlistRSSCAN_ALLOWLIST.relayshield-allowlistFingerprints to ignore.
--reportRSSCAN_REPORT(off)Write a shareable exposure report to this path.
--orgRSSCAN_ORG(off)Opt in to reporting adoption for your org domain.
--strict / --no-strictRSSCAN_STRICTsee belowFail when the scan cannot run.
--annotateRSSCAN_ANNOTATEautoInline PR annotations. auto enables them inside GitHub Actions only; github forces; off disables.
--slack-webhookRSSCAN_SLACK_WEBHOOK(off)POST a findings summary to your own Slack incoming webhook.
--webhookRSSCAN_WEBHOOK(off)POST findings as JSON to an endpoint you control.

Failure behaviour differs by mode, on purpose

Pre-commit fails open. If the scan genuinely cannot run (an unreadable diff, a broken git invocation), it warns on stderr and lets the commit through. A hook that wedges every commit gets uninstalled, and then it catches nothing.

CI fails closed. A gate that silently reports success when it could not actually run is worse than no gate. It manufactures false assurance. Override either way with --strict / --no-strict or RSSCAN_STRICT.

Allowlisting

The allowlist holds fingerprints, not secrets: a file containing the actual values would be the same mistake this tool exists to prevent.

# .relayshield-allowlist
sha256:1a5d44a2dca19669 # documented example key in docs/quickstart.md

Sending findings somewhere

Two push channels, both opt-in, both pointing at somewhere you own. (The third delivery channel, inline PR annotations, is automatic in GitHub Actions; see above.) Without one of these flags rsscan makes no network call at all.

# Slack
rsscan --slack-webhook https://hooks.slack.com/services/T000/B000/xxxx
# Any JSON receiver you control
rsscan --webhook https://hooks.example.com/rsscan

None of them ever carries a secret value. Each finding is transmitted as its credential type, severity, file, line and fingerprint. That is the same guarantee as --report. The Slack message says so in its own footer, so whoever reads the channel knows it is safe to leave there.

They fire only when there are findings. A notification on every clean build trains people to ignore the channel, which is how a real finding gets missed.

Delivery failure never changes the exit code. If Slack is unreachable, rsscan warns on stderr and the build result stands on the scan alone: a gate should block on secrets, not on a flaky notification endpoint.

The generic webhook posts:

{
"tool": "rsscan", "version": "0.2.0", "scanned": "origin/main...HEAD",
"repo": "acme/api", "ref": "feature/pay", "build_url": "https://github.com/...",
"findings_count": 2, "blocking_count": 2, "highest_severity": "CRITICAL",
"severity_counts": {"CRITICAL": 2},
"findings": [
{"type": "aws_access_key", "severity": "CRITICAL", "description": "AWS IAM Access Key",
"file": "src/config.py", "line": 3, "fingerprint": "sha256:1a5d44a2dca19669"}
],
"detected_at": "2026-08-04T15:00:00+00:00"
}

Counting who can publish your dependencies

rsscan --deps # auto-detects package-lock.json, then package.json
rsscan --deps path/to/package-lock.json # or point it at one

A self-replicating npm worm does not start with malicious code. It starts with a maintainer account: an infostealer takes the publish token out of somebody's .npmrc, and a patch version nobody reads gets published four steps before there is any artifact for a scanner to analyse.

--deps tells you how large that surface is for your own tree:

 Who can publish your dependencies
433 dependencies in package-lock.json
275 distinct publisher accounts can push code into them
126 on personal webmail (no SSO, no central revocation)
28 role or automation addresses

Each package is resolved to its maintainers list plus the _npmUser who actually published the version you would install. Counts are of distinct email addresses, which is a proxy for accounts and imperfect in both directions: one person with two addresses counts twice, two people sharing one count once.

It reads locally and queries only registry.npmjs.org. No account, no API key, no network call to RelayShield, and no telemetry. It prints integers and names nobody.

It always exits 0. There is no dependency count that constitutes a build failure, so this is a report and deliberately not a gate.

If a package cannot be resolved, that count is printed separately and is not folded into the totals. A package whose publishers we could not look up is not a package with no publishers, and collapsing those two into one number is how a tool ends up quietly reassuring you.

Sharing a finding with your security team

rsscan --report exposure.md

Writes a Markdown report you can attach to a ticket or forward by email. It contains no secret values, only fingerprints, so it is safe to share.

Optional: telling us your org uses rsscan

rsscan --org yourcompany.com

Off by default and entirely optional. When enabled it sends only your org domain, an anonymous per-machine id, the tool version, and how many findings there were by severity.

It never sends file paths, fingerprints, repository names, source code, or the secrets themselves. There is no mechanism in the tool to do so.

What this tool cannot tell you

rsscan stops credentials before they enter git history. It cannot see credentials that have already left: a key committed last year, or one leaked through a dependency, a published package or a container image, may already be indexed and scraped.

Answering that needs a view of what is public, plus the identity layer secret scanners do not cover at all: workforce credentials surfacing in infostealer logs and breach dumps, SIM-swap risk on staff accounts, and session/token exposure. That is what RelayShield does.

Pricing

rsscan is free. There is no paid tier of this tool, no scan quota, and no account.

Licence

MIT

About

Block commits that introduce API keys, tokens and other machine credentials. Runs entirely locally.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Use this GitHub action with your project
Add this Action to an existing workflow or create a new one
View on Marketplace

Repository files navigation

rsscan

PyPI is the published version of record. Block commits and builds that introduce API keys, tokens and other machine credentials.

Detects 31 credential patterns: AWS IAM keys, GitHub PATs, Stripe secrets, Slack tokens, private keys, and LLM provider keys (OpenAI, Anthropic, Google, Groq, xAI, Replicate).

Free, and it runs entirely on your machine. No account, no API key, no network call. Your source code never leaves the host. Matching happens locally against patterns shipped inside the package.

The pre-commit hook is the point. It runs before the commit enters git history. A CI check only sees the secret after a push, and by then it is in history and has to be rotated even if you delete the commit. The CI integrations below are a backstop, not a substitute.

New in 0.2.0:rsscan --deps counts the accounts that can publish into your npm dependencies. See Counting who can publish your dependencies.

Install

pre-commit hook (recommended)

# .pre-commit-config.yamlrepos:
- repo: https://github.com/RelayShield/rsscanrev: v0.2.0hooks:
- id: rsscan
pre-commit install

That is the whole setup. Nothing to configure, nothing to sign up for.

GitHub Actions

- uses: actions/checkout@v4with:
fetch-depth: 0# required: the range needs history
- uses: RelayShield/rsscan@v0.2.0with:
fail-on: HIGH

Findings are annotated inline on the changed lines in the pull request's Files tab, so a developer sees them where the code is rather than in collapsed build output. Blocking findings (at or above fail-on) appear as errors; everything else appears as warnings.

This needs no token, no permissions: block and no GitHub App. Annotations are emitted as workflow commands, not through the Checks API. Turn them off with annotate: off if you only want the log.

Annotations never contain the secret value. Each one carries the credential type, its severity, and the fingerprint you would add to .relayshield-allowlist to suppress a false positive.

GitLab CI/CD

rsscan:
image: relayshield/rsscan:0.2.0variables:
GIT_DEPTH: 0# required: GitLab shallow-clones, and a shallow clone# yields an empty diff, so the job would pass having# scanned nothingRSSCAN_REV_RANGE: "origin/$CI_DEFAULT_BRANCH...HEAD"RSSCAN_FAIL_ON: HIGHscript: ["rsscan"]

CircleCI

orbs:
rsscan: relayshield/rsscan@0.1.0workflows:
main:
jobs:
- rsscan/scan

Docker: Bitbucket Pipelines, Tekton, Drone, Woodpecker, Harness, anything else

docker run --rm -v "$PWD:/workspace" \
-e RSSCAN_REV_RANGE=origin/main...HEAD \
relayshield/rsscan:0.2.0

Bitbucket Pipelines:

- step:
script:
- pipe: docker://relayshield/rsscan:0.2.0variables:
RSSCAN_REV_RANGE: "origin/main...HEAD"

Jenkins, Azure DevOps, or any shell

pip install rsscan
RSSCAN_REV_RANGE="origin/main...HEAD" rsscan

How it works

Scans the diff locally and fails on a finding at or above --fail-on (default HIGH).

Only added lines are scanned. Secrets already in your files are not re-flagged, so the tool does not become unbypassable noise on a repo with legacy findings.

Nothing is transmitted and nothing is printed. There is no scan endpoint to send code to. Matched values never appear in output either: findings carry a file, a line and a non-reversible fingerprint, so a scan is safe to run in CI without leaking the secret into build logs.

 rsscan: secrets detected in staged changes
CRITICAL AWS IAM Access Key
src/config.py:14
fingerprint sha256:1a5d44a2dca19669
Commit refused.
Remove the value and load it from a secrets manager or environment
variable instead. If it has already left this machine, rotate it.
False positive? Add the fingerprint to .relayshield-allowlist:
echo 'sha256:1a5d44a2dca19669' >> .relayshield-allowlist
To bypass entirely: git commit --no-verify

Configuration

Every flag has an environment variable equivalent, which is how the CI clients drive it.

FlagEnv varDefaultMeaning
--fail-onRSSCAN_FAIL_ONHIGHLowest severity that fails. LOW/MEDIUM/HIGH/CRITICAL.
--rev-rangeRSSCAN_REV_RANGE(staged)Scan a commit range instead of staged changes.
--allowlistRSSCAN_ALLOWLIST.relayshield-allowlistFingerprints to ignore.
--reportRSSCAN_REPORT(off)Write a shareable exposure report to this path.
--orgRSSCAN_ORG(off)Opt in to reporting adoption for your org domain.
--strict / --no-strictRSSCAN_STRICTsee belowFail when the scan cannot run.
--annotateRSSCAN_ANNOTATEautoInline PR annotations. auto enables them inside GitHub Actions only; github forces; off disables.
--slack-webhookRSSCAN_SLACK_WEBHOOK(off)POST a findings summary to your own Slack incoming webhook.
--webhookRSSCAN_WEBHOOK(off)POST findings as JSON to an endpoint you control.

Failure behaviour differs by mode, on purpose

Pre-commit fails open. If the scan genuinely cannot run (an unreadable diff, a broken git invocation), it warns on stderr and lets the commit through. A hook that wedges every commit gets uninstalled, and then it catches nothing.

CI fails closed. A gate that silently reports success when it could not actually run is worse than no gate. It manufactures false assurance. Override either way with --strict / --no-strict or RSSCAN_STRICT.

Allowlisting

The allowlist holds fingerprints, not secrets: a file containing the actual values would be the same mistake this tool exists to prevent.

# .relayshield-allowlist
sha256:1a5d44a2dca19669 # documented example key in docs/quickstart.md

Sending findings somewhere

Two push channels, both opt-in, both pointing at somewhere you own. (The third delivery channel, inline PR annotations, is automatic in GitHub Actions; see above.) Without one of these flags rsscan makes no network call at all.

# Slack
rsscan --slack-webhook https://hooks.slack.com/services/T000/B000/xxxx
# Any JSON receiver you control
rsscan --webhook https://hooks.example.com/rsscan

None of them ever carries a secret value. Each finding is transmitted as its credential type, severity, file, line and fingerprint. That is the same guarantee as --report. The Slack message says so in its own footer, so whoever reads the channel knows it is safe to leave there.

They fire only when there are findings. A notification on every clean build trains people to ignore the channel, which is how a real finding gets missed.

Delivery failure never changes the exit code. If Slack is unreachable, rsscan warns on stderr and the build result stands on the scan alone: a gate should block on secrets, not on a flaky notification endpoint.

The generic webhook posts:

{
"tool": "rsscan", "version": "0.2.0", "scanned": "origin/main...HEAD",
"repo": "acme/api", "ref": "feature/pay", "build_url": "https://github.com/...",
"findings_count": 2, "blocking_count": 2, "highest_severity": "CRITICAL",
"severity_counts": {"CRITICAL": 2},
"findings": [
{"type": "aws_access_key", "severity": "CRITICAL", "description": "AWS IAM Access Key",
"file": "src/config.py", "line": 3, "fingerprint": "sha256:1a5d44a2dca19669"}
],
"detected_at": "2026-08-04T15:00:00+00:00"
}

Counting who can publish your dependencies

rsscan --deps # auto-detects package-lock.json, then package.json
rsscan --deps path/to/package-lock.json # or point it at one

A self-replicating npm worm does not start with malicious code. It starts with a maintainer account: an infostealer takes the publish token out of somebody's .npmrc, and a patch version nobody reads gets published four steps before there is any artifact for a scanner to analyse.

--deps tells you how large that surface is for your own tree:

 Who can publish your dependencies
433 dependencies in package-lock.json
275 distinct publisher accounts can push code into them
126 on personal webmail (no SSO, no central revocation)
28 role or automation addresses

Each package is resolved to its maintainers list plus the _npmUser who actually published the version you would install. Counts are of distinct email addresses, which is a proxy for accounts and imperfect in both directions: one person with two addresses counts twice, two people sharing one count once.

It reads locally and queries only registry.npmjs.org. No account, no API key, no network call to RelayShield, and no telemetry. It prints integers and names nobody.

It always exits 0. There is no dependency count that constitutes a build failure, so this is a report and deliberately not a gate.

If a package cannot be resolved, that count is printed separately and is not folded into the totals. A package whose publishers we could not look up is not a package with no publishers, and collapsing those two into one number is how a tool ends up quietly reassuring you.

Sharing a finding with your security team

rsscan --report exposure.md

Writes a Markdown report you can attach to a ticket or forward by email. It contains no secret values, only fingerprints, so it is safe to share.

Optional: telling us your org uses rsscan

rsscan --org yourcompany.com

Off by default and entirely optional. When enabled it sends only your org domain, an anonymous per-machine id, the tool version, and how many findings there were by severity.

It never sends file paths, fingerprints, repository names, source code, or the secrets themselves. There is no mechanism in the tool to do so.

What this tool cannot tell you

rsscan stops credentials before they enter git history. It cannot see credentials that have already left: a key committed last year, or one leaked through a dependency, a published package or a container image, may already be indexed and scraped.

Answering that needs a view of what is public, plus the identity layer secret scanners do not cover at all: workforce credentials surfacing in infostealer logs and breach dumps, SIM-swap risk on staff accounts, and session/token exposure. That is what RelayShield does.

Pricing

rsscan is free. There is no paid tier of this tool, no scan quota, and no account.

Licence

MIT

About

Block commits that introduce API keys, tokens and other machine credentials. Runs entirely locally.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages