Compare commits

..
Author SHA1 Message Date
george-keygraphandClaude Opus 5.5 9f04aee2c3 docs: add UTM tags to keygraph.io links
Tag every keygraph.io link in the README, the npm README, and the PDF
report template with the Keygraph UTM vocabulary so Plausible can
attribute site traffic by placement.

- README.md: 7 links, utm_source=github, utm_medium=referral
- apps/cli/README.md: 3 links, utm_source=npm (shows on npm after the
  next publish)
- report.typ: keygraph-url, utm_source=shannon, utm_medium=product
  (header wordmark and cover "Tester" line in every PDF report)

All use utm_campaign=shannon_oss_2026q4; utm_content names the placement.

llms.txt, llms-full.txt and docs/ are left untagged on purpose: they are
read by AI engines, and tagged URLs there would be repeated in AI answers
and misattributed. Keep llms-full.txt untagged when regenerating it from
the README.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:43:36 -07:00
5 changed files with 33 additions and 40 deletions

No files matched your search

+7 -7
View File
@@ -27,7 +27,7 @@ npx @keygraph/shannon@latest
---
<a href="https://discord.gg/9ZqQPuhJB7"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/discord_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/discord_button_light.png"><img src="./assets/discord_button_light.png" height="40" alt="Join Discord"></picture></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="https://keygraph.io/"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/keygraph_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/keygraph_button_light.png"><img src="./assets/keygraph_button_light.png" height="40" alt="Visit Keygraph.io"></picture></a>
<a href="https://discord.gg/9ZqQPuhJB7"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/discord_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/discord_button_light.png"><img src="./assets/discord_button_light.png" height="40" alt="Join Discord"></picture></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="https://keygraph.io/?utm_source=github&amp;utm_medium=referral&amp;utm_campaign=shannon_oss_2026q4&amp;utm_content=readme_header_button"><picture><source media="(prefers-color-scheme: dark)" srcset="./assets/keygraph_button_dark.png"><source media="(prefers-color-scheme: light)" srcset="./assets/keygraph_button_light.png"><img src="./assets/keygraph_button_light.png" height="40" alt="Visit Keygraph.io"></picture></a>
---
@@ -72,11 +72,11 @@ npx @keygraph/shannon@latest
## What is Shannon?
Shannon is an autonomous AI pentester developed by [Keygraph](https://keygraph.io). It performs security testing of web applications and their underlying APIs by combining source-code analysis with live exploitation.
Shannon is an autonomous AI pentester developed by [Keygraph](https://keygraph.io/?utm_source=github&utm_medium=referral&utm_campaign=shannon_oss_2026q4&utm_content=readme_intro). It performs security testing of web applications and their underlying APIs by combining source-code analysis with live exploitation.
Shannon analyzes your web application's source code to identify potential attack vectors, then uses browser automation and command-line tools to execute real exploits against the running application and its APIs. Only vulnerabilities with a working proof-of-concept are included in the final report.
Shannon is the agent. This repository is Shannon, the open-source pentester you run yourself. The same Shannon also powers Keygraph's commercial editions, [Pro and Enterprise](https://keygraph.io/pricing). See [Editions](#editions) for how Shannon compares with Pro, Enterprise, and the Community Program.
Shannon is the agent. This repository is Shannon, the open-source pentester you run yourself. The same Shannon also powers Keygraph's commercial editions, [Pro and Enterprise](https://keygraph.io/pricing?utm_source=github&utm_medium=referral&utm_campaign=shannon_oss_2026q4&utm_content=readme_intro_pricing). See [Editions](#editions) for how Shannon compares with Pro, Enterprise, and the Community Program.
<a id="why-shannon-exists"></a>
<details>
@@ -242,7 +242,7 @@ See the [Shannon GitHub Action documentation](https://github.com/KeygraphHQ/shan
| **Pro** | Keygraph's cloud-hosted edition, managed by Keygraph, with every module included. | $50 per active developer per month. |
| **Enterprise** | Everything in Pro, self-hosted in your own environment or fully air-gapped. | Custom. |
See [keygraph.io/pricing](https://keygraph.io/pricing) for current prices and the full feature table.
See [keygraph.io/pricing](https://keygraph.io/pricing?utm_source=github&utm_medium=referral&utm_campaign=shannon_oss_2026q4&utm_content=readme_editions_pricing) for current prices and the full feature table.
**Pro**, **Enterprise**, and the **Community Program** are powered by an enhanced, enterprise-grade version of the Shannon engine, and all three add:
@@ -252,7 +252,7 @@ See [keygraph.io/pricing](https://keygraph.io/pricing) for current prices and th
- **Fix pull requests and retests**: reviewable fix pull requests, with each fix verified by re-analysis and exploit replay, without a full rescan.
- **Jira and access control**: two-way Jira sync, SSO, RBAC, and audit logs.
No source code? Use the [Blackbox Pentester](https://keygraph.io/agentic-blackbox-pentester).
No source code? Use the [Blackbox Pentester](https://keygraph.io/agentic-blackbox-pentester?utm_source=github&utm_medium=referral&utm_campaign=shannon_oss_2026q4&utm_content=readme_editions_blackbox).
[Compare Shannon, Pro, and Enterprise in detail →](docs/keygraph-platform.md)
@@ -364,7 +364,7 @@ At this time, Keygraph is not accepting external code contributions. Issues are
Stay connected:
- [Keygraph website](https://keygraph.io)
- [Keygraph website](https://keygraph.io/?utm_source=github&utm_medium=referral&utm_campaign=shannon_oss_2026q4&utm_content=readme_community_website)
- [Twitter/X: @KeygraphHQ](https://twitter.com/KeygraphHQ)
- [LinkedIn: Keygraph](https://linkedin.com/company/keygraph)
@@ -400,4 +400,4 @@ Shannon works with local models served through Ollama, vLLM, or LM Studio, which
Shannon executes real exploits. It reports a finding only when it has produced a working proof-of-concept, and discards hypotheses it cannot prove. It is a pentester, not a passive scanner.
**Built by [Keygraph](https://keygraph.io)**
**Built by [Keygraph](https://keygraph.io/?utm_source=github&utm_medium=referral&utm_campaign=shannon_oss_2026q4&utm_content=readme_footer)**
+3 -3
View File
@@ -10,7 +10,7 @@ It analyzes your source code, identifies attack paths, and executes real exploit
---
<a href="https://discord.gg/9ZqQPuhJB7"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/discord_button_light.png" height="40" alt="Join Discord"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="https://keygraph.io/"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/keygraph_button_light.png" height="40" alt="Visit Keygraph.io"></a>
<a href="https://discord.gg/9ZqQPuhJB7"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/discord_button_light.png" height="40" alt="Join Discord"></a>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="https://keygraph.io/?utm_source=npm&amp;utm_medium=referral&amp;utm_campaign=shannon_oss_2026q4&amp;utm_content=readme_header_button"><img src="https://raw.githubusercontent.com/KeygraphHQ/shannon/main/assets/keygraph_button_light.png" height="40" alt="Visit Keygraph.io"></a>
---
@@ -41,7 +41,7 @@ Shannon pulls the worker image from Docker Hub, starts the required local infras
## Editions
**Shannon** is this package: the open-source pentester you run yourself, on demand, and complete in that lane. **Keygraph Pro** and **Keygraph Enterprise** are the commercial editions. They run an enhanced build of Shannon continuously and close the full AppSec lifecycle around it: code analysis, finding management, automated remediation, verification, and enterprise deployment. The **Community Program** offers Pro at no cost to organizations that qualify. See [keygraph.io/pricing](https://keygraph.io/pricing).
**Shannon** is this package: the open-source pentester you run yourself, on demand, and complete in that lane. **Keygraph Pro** and **Keygraph Enterprise** are the commercial editions. They run an enhanced build of Shannon continuously and close the full AppSec lifecycle around it: code analysis, finding management, automated remediation, verification, and enterprise deployment. The **Community Program** offers Pro at no cost to organizations that qualify. See [keygraph.io/pricing](https://keygraph.io/pricing?utm_source=npm&utm_medium=referral&utm_campaign=shannon_oss_2026q4&utm_content=readme_editions_pricing).
## Documentation
@@ -56,5 +56,5 @@ Commercial and enterprise licensing is available for organizations that need dif
For commercial licensing, contact [shannon@keygraph.io](mailto:shannon@keygraph.io).
<p align="center">
<b>Built by <a href="https://keygraph.io">Keygraph</a></b>
<b>Built by <a href="https://keygraph.io/?utm_source=npm&amp;utm_medium=referral&amp;utm_campaign=shannon_oss_2026q4&amp;utm_content=readme_footer">Keygraph</a></b>
</p>
+21 -28
View File
@@ -184,9 +184,6 @@ async function waitForWorkflowClosure(
/**
* Request cooperative cancellation, then make at most two termination attempts when the
* workflow does not close during its grace period. Every success is backed by direct state.
*
* A grace of zero skips the cooperative phase and terminates directly: a workflow with no live
* worker can never honour a cooperative cancel, so callers pass `graceMs = 0` for those targets.
*/
export async function stopWorkflowCancelFirst(
workflowId: string,
@@ -195,16 +192,14 @@ export async function stopWorkflowCancelFirst(
pollMs: number = CANCELLATION_POLL_MS,
verifyMs: number = TERMINATION_VERIFY_MS,
): Promise<WorkflowStopOutcome> {
if (graceMs > 0) {
try {
if ((await lifecycle.cancel(workflowId)) === 'not-found') return { kind: 'already-closed' };
} catch {
// The request may have reached Temporal even when its acknowledgement was lost.
}
try {
if ((await lifecycle.cancel(workflowId)) === 'not-found') return { kind: 'already-closed' };
} catch {
// The request may have reached Temporal even when its acknowledgement was lost.
}
if (await waitForWorkflowClosure(workflowId, lifecycle, lifecycle.now() + graceMs, pollMs)) {
return { kind: 'graceful' };
}
if (await waitForWorkflowClosure(workflowId, lifecycle, lifecycle.now() + graceMs, pollMs)) {
return { kind: 'graceful' };
}
const verifyPerAttemptMs = Math.max(pollMs, Math.ceil(verifyMs / TERMINATION_ATTEMPTS));
@@ -230,21 +225,16 @@ export async function stopWorkflowCancelFirst(
return { kind: 'unverified' };
}
/**
* Apply the bounded lifecycle concurrently to a captured set of targets, preserving input order in
* the returned outcomes. A target with no live worker (`containerCandidate === false`) gets no grace.
*/
/** Apply the same bounded lifecycle concurrently to a captured set of workflow IDs. */
export async function stopWorkflowsCancelFirst(
targets: readonly WorkflowStopTarget[],
workflowIds: readonly string[],
lifecycle: StopLifecycle = stopLifecycle,
graceMs: number = CANCELLATION_GRACE_MS,
pollMs: number = CANCELLATION_POLL_MS,
verifyMs: number = TERMINATION_VERIFY_MS,
): Promise<readonly WorkflowStopOutcome[]> {
const settlements = await Promise.allSettled(
targets.map((target) =>
stopWorkflowCancelFirst(target.workflowId, lifecycle, target.containerCandidate ? graceMs : 0, pollMs, verifyMs),
),
workflowIds.map((workflowId) => stopWorkflowCancelFirst(workflowId, lifecycle, graceMs, pollMs, verifyMs)),
);
return settlements.map((settlement) =>
settlement.status === 'fulfilled' ? settlement.value : { kind: 'unverified' },
@@ -388,7 +378,13 @@ export async function executeStopPlan(
verifyMs: number = TERMINATION_VERIFY_MS,
candidateSettleMs: number = CANDIDATE_REGISTRATION_SETTLE_MS,
): Promise<StopExecutionResult> {
const initialOutcomes = await stopWorkflowsCancelFirst(targets, lifecycle, graceMs, pollMs, verifyMs);
const initialOutcomes = await stopWorkflowsCancelFirst(
targets.map((target) => target.workflowId),
lifecycle,
graceMs,
pollMs,
verifyMs,
);
const outcomes = new Map<string, WorkflowStopOutcome>();
for (let index = 0; index < targets.length; index++) {
const target = targets[index];
@@ -721,7 +717,10 @@ export async function stopVisibleWorkflowsUntilSettled(
recheckedAlreadyClosed.add(target.workflowId);
}
}
const outcomes = await stopWorkflowsCancelFirst(residualTargets, lifecycle);
const outcomes = await stopWorkflowsCancelFirst(
residualTargets.map((target) => target.workflowId),
lifecycle,
);
for (let index = 0; index < residualTargets.length; index++) {
const target = residualTargets[index];
const outcome = outcomes[index];
@@ -760,12 +759,6 @@ async function stopAllScans(yes: boolean): Promise<void> {
if (initialDiscovery.kind === 'unavailable') {
fail('Could not verify whether scan workflows are running in Temporal.', `Retry: ${commandPrefix()} stop --all`);
}
// The settle loop below only catches a scan whose workflow is mid-launch and not yet visible,
// and such a launch always leaves a pending record. With none, nothing can appear — skip it.
if (pending.references.length === 0) {
console.log('No running scans to stop.');
return;
}
const emptySettleDeadline = stopLifecycle.now() + VISIBILITY_MAX_SETTLE_MS;
while (targets.length === 0) {
const remaining = emptySettleDeadline - stopLifecycle.now();
+1 -1
View File
@@ -583,7 +583,7 @@ export function runningScanContainersChecked(
filter: readonly string[] = WORKER_FILTER,
): CommandQueryResult<RunningScanContainer[]> {
try {
const format = `{{.ID}}\t{{.Label "${WORKSPACE_LABEL}"}}\t{{.Label "${TASK_QUEUE_LABEL}"}}\t{{.Label "${WORKFLOW_ID_LABEL}"}}\t{{.Label "${WORKER_PROTOCOL_LABEL}"}}`;
const format = `{{.ID}}\t{{ index .Labels "${WORKSPACE_LABEL}" }}\t{{ index .Labels "${TASK_QUEUE_LABEL}" }}\t{{ index .Labels "${WORKFLOW_ID_LABEL}" }}\t{{ index .Labels "${WORKER_PROTOCOL_LABEL}" }}`;
const output = execFileSync('docker', ['ps', ...filter, '--format', format], {
stdio: 'pipe',
encoding: 'utf-8',
+1 -1
View File
@@ -17,7 +17,7 @@
#let tester-override = sys.inputs.at("tester", default: "Shannon")
#let brand = sys.inputs.at("brand", default: "Shannon | AI Pentester by Keygraph")
#let keygraph-url = "https://keygraph.io"
#let keygraph-url = "https://keygraph.io/?utm_source=shannon&utm_medium=product&utm_campaign=shannon_oss_2026q4&utm_content=pdf_report"
// ---------- Palette ---------------------------------------------------------
// Kept distinct so Critical / High are not confused under monitor gamma.