diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..50015e0 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,78 @@ +# Security Policy + +CronStream is a financial protocol. It custodies funds in smart contracts and +signs on-chain payment authorizations off-chain. We take security seriously and +appreciate responsible disclosure from the community. + +## Reporting a vulnerability + +**Do not open a public GitHub issue, pull request, or discussion for a security +vulnerability.** Public disclosure before a fix puts user funds at risk. + +Report privately to the maintainers: + +- **Email:** thecronstream@gmail.com +- **Telegram:** [@AbrahamNA_VIG](https://t.me/AbrahamNA_VIG) + +Please include: + +- A description of the vulnerability and its impact +- Steps to reproduce (proof-of-concept, transaction hashes, or code references) +- Affected component(s) and version/commit +- Any suggested remediation + +If you can, encrypt sensitive details or share a minimal private repro rather +than posting exploit code anywhere public. + +## What to expect + +- **Acknowledgement** within 72 hours of your report. +- **Triage and severity assessment** shortly after, with a planned remediation + timeline communicated to you. +- **Coordinated disclosure.** We will work with you on timing and credit you in + the fix notes unless you prefer to remain anonymous. + +Please give us a reasonable window to remediate before any public disclosure. + +## Scope + +In scope: + +- **Smart contracts** (`contracts/`): fund custody, stream accounting, voucher + verification, nonce/replay protection, access control, reclaim/cancel logic. +- **Agent node** (`agent-node/`): EIP-712 voucher signing, milestone + verification, webhook signature validation, API authentication, rate limiting, + credential encryption, and the public x402 API. +- **Frontend** (`frontend/`): issues that can lead to loss of funds, signature + phishing, or auth bypass. + +Examples of high-value reports: + +- Signing or submitting an extension voucher without genuine verified work +- Replay or nonce reuse against the router contract +- Reclaiming or withdrawing funds the caller is not entitled to +- Webhook signature bypass that lets an attacker forge verification events +- Leakage of stored OAuth tokens / API keys, or encryption weaknesses +- Authentication or rate-limit bypass on the agent API + +## Out of scope + +- Vulnerabilities in third-party dependencies already tracked by Dependabot + (please still report if you have a working exploit against CronStream). +- Spam, automated scanner output, missing best-practice headers with no + demonstrable impact, or social-engineering of maintainers. +- Issues requiring a compromised user device or a malicious privileged operator. +- Testnet-only griefing with no mainnet impact. + +## Safe harbor + +We support good-faith security research. If you make a genuine effort to follow +this policy (avoid privacy violations, data destruction, and service +degradation, and only test against assets you control or testnets), we will not +pursue or support legal action against you for your research. + +## A note on the license + +CronStream is released under the [Business Source License 1.1](./LICENSE). The +security of the protocol is a shared interest regardless of license terms, and +responsible disclosure is always welcome. diff --git a/agent-node/src/codeDiff.js b/agent-node/src/codeDiff.js new file mode 100644 index 0000000..6be120b --- /dev/null +++ b/agent-node/src/codeDiff.js @@ -0,0 +1,60 @@ +/** + * codeDiff.js + * Shared "is this a real code change?" check for milestone verification. + * + * A merged PR counts as a deliverable if it adds real code — regardless of where + * the repo keeps that code. We use a DENYLIST (not a `src/`+`contracts/` + * allowlist) so it works for every project layout and every developer's + * workflow: Go in cmd/, Python at root, JS in lib/ or packages/, etc. + * + * A file is NOT counted when it is documentation, lockfiles, CI config, build + * config, or binary assets — changes to those alone shouldn't trigger payment. + */ + +const DOC_EXTS = new Set([ + '.md', '.txt', '.mdx', '.rst', '.adoc', +]); + +const ASSET_EXTS = new Set([ + '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.webp', '.bmp', '.avif', + '.woff', '.woff2', '.ttf', '.eot', '.otf', '.mp4', '.mov', '.webm', '.pdf', +]); + +// Build/CI/config formats — a PR touching only these isn't a code deliverable. +const CONFIG_EXTS = new Set([ + '.json', '.yml', '.yaml', '.toml', '.ini', '.cfg', '.conf', + '.lock', '.env', '.editorconfig', '.log', '.map', '.snap', +]); + +// Non-code metadata files that have no extension. +const IGNORE_BASENAMES = new Set([ + 'license', 'copying', 'notice', 'authors', 'codeowners', 'changelog', +]); + +/** + * @param {string} filename - path as reported by the provider (e.g. "agent-node/src/db.js") + * @returns {boolean} true if the change should count toward a milestone + */ +export function isQualifyingCodeFile(filename) { + if (!filename) return false; + const path = filename.toLowerCase(); + + // CI / workflow definitions never count on their own. + if (path.startsWith('.github/') || path.includes('/.github/')) return false; + + const base = path.split('/').pop(); + if (!base) return false; + + // Dotfiles (.gitignore, .env, .prettierrc, .babelrc, …) are config by convention. + if (base.startsWith('.')) return false; + if (IGNORE_BASENAMES.has(base)) return false; + if (base.endsWith('.lock')) return false; + + const lastDot = base.lastIndexOf('.'); + const ext = lastDot >= 0 ? base.slice(lastDot) : ''; + if (DOC_EXTS.has(ext) || ASSET_EXTS.has(ext) || CONFIG_EXTS.has(ext)) return false; + + // Everything else (.js .ts .tsx .sol .py .go .rs .java .rb .php .c .cpp .sh, + // Dockerfile, Makefile, …) is real code. + return true; +} diff --git a/agent-node/src/verificationEngine.js b/agent-node/src/verificationEngine.js index bdb4f1b..0305494 100644 --- a/agent-node/src/verificationEngine.js +++ b/agent-node/src/verificationEngine.js @@ -18,12 +18,11 @@ import { getLastExtensionTime, isAlreadyProcessed, recordExtension, getProfile, import { readStreamBatch, submitExtension } from './chainSubmitter.js'; import { signExtensionVoucher } from './agentSigner.js'; import { getInstallationToken } from './githubApp.js'; +import { isQualifyingCodeFile } from './codeDiff.js'; const WARN_WINDOW_S = 48 * 3600; // top up streams expiring within 48h const FROZEN_LOOKBACK_S = 7 * 24 * 3600; // ignore streams frozen more than 7 days ago const GITHUB_API_BASE = 'https://api.github.com'; -const EXCLUDED_EXTS = ['.md', '.txt', '.mdx', '.rst']; -const SOURCE_PREFIXES = ['src/', 'contracts/']; const VOUCHER_TTL_S = Number(process.env.VOUCHER_TTL_SECONDS ?? 3600); // ─── GitHub helpers ─────────────────────────────────────────────────────────── @@ -45,11 +44,7 @@ async function ghGet(path, token) { } function hasQualifyingDiff(files) { - return files.some(f => - f.additions > 0 && - !EXCLUDED_EXTS.some(ext => f.filename.toLowerCase().endsWith(ext)) && - SOURCE_PREFIXES.some(p => f.filename.includes(`/${p}`) || f.filename.startsWith(p)), - ); + return files.some(f => f.additions > 0 && isQualifyingCodeFile(f.filename)); } // ─── GitHub webhook verification ───────────────────────────────────────────── @@ -176,14 +171,11 @@ export function verifyJiraWebhook(payload, contractorProfile) { // ─── Bitbucket webhook verification ────────────────────────────────────────── -const BB_CODE_PATH_RE = /^(src|contracts|lib|packages)\//; -const BB_IGNORE_EXTS = new Set(['.md', '.txt', '.json', '.lock', '.yml', '.yaml', '.mdx']); - /** * Verify a Bitbucket `pullrequest:fulfilled` webhook payload. * 3-layer gate: * 1. PR author matches the contractor's registered Bitbucket username / UUID - * 2. PR contains real code changes in /src or /contracts (checked via diffstat API) + * 2. PR contains real code changes (any non-doc/config file, checked via diffstat API) * 3. Latest pipeline on the merge commit passed * * @param {object} payload - raw Bitbucket webhook body @@ -232,11 +224,7 @@ export async function verifyBitbucketWebhook(payload, companyCredentials, contra if (!diffRes.ok) return { ok: false, reason: `Bitbucket diffstat API returned ${diffRes.status}` }; const diffData = await diffRes.json(); const files = diffData.values ?? []; - const hasCode = files.some(f => { - const path = f.new?.path ?? f.old?.path ?? ''; - const ext = path.includes('.') ? '.' + path.split('.').pop() : ''; - return BB_CODE_PATH_RE.test(path) && !BB_IGNORE_EXTS.has(ext); - }); + const hasCode = files.some(f => isQualifyingCodeFile(f.new?.path ?? f.old?.path ?? '')); if (!hasCode) { return { ok: false, reason: `No qualifying code changes across ${files.length} file(s)` }; } diff --git a/agent-node/src/verifyMilestone.js b/agent-node/src/verifyMilestone.js index 819be94..f633b18 100644 --- a/agent-node/src/verifyMilestone.js +++ b/agent-node/src/verifyMilestone.js @@ -3,7 +3,7 @@ * Multi-source milestone verification for the CronStream agent node. * * Sources: - * github — 3 layers: PR merged + CI green + code diff in /src or /contracts + * github — 3 layers: PR merged + CI green + real code diff (any non-doc file) * jira — ticket statusCategory is 'done' * bitbucket — PR merged + optional pipeline success * figma — approval comment (approved / lgtm / ✅) within 30 days @@ -14,6 +14,8 @@ * platform API keys required. */ +import { isQualifyingCodeFile } from './codeDiff.js'; + // ─── Custom Error ───────────────────────────────────────────────────────────── export class VerificationError extends Error { @@ -33,8 +35,6 @@ export class VerificationError extends Error { // ───────────────────────────────────────────────────────────────────────────── const GITHUB_API_BASE = 'https://api.github.com'; -const EXCLUDED_EXTENSIONS = ['.md', '.txt', '.mdx', '.rst']; -const SOURCE_PATH_PREFIXES = ['src/', 'contracts/']; async function githubGet(path, token) { const resolvedToken = token ?? process.env.GITHUB_TOKEN; @@ -85,19 +85,14 @@ async function checkCodeDiff(owner, repo, prNumber, token) { page++; } - const qualifying = allFiles.filter(file => { - if (!file.additions || file.additions === 0) return false; - const filename = file.filename.toLowerCase(); - if (EXCLUDED_EXTENSIONS.some(ext => filename.endsWith(ext))) return false; - return SOURCE_PATH_PREFIXES.some( - prefix => filename.includes(`/${prefix}`) || filename.startsWith(prefix), - ); - }); + const qualifying = allFiles.filter( + file => file.additions > 0 && isQualifyingCodeFile(file.filename), + ); if (qualifying.length === 0) { throw new VerificationError( 1, - 'No qualifying code changes — need ≥1 addition in /src or /contracts (excluding .md/.txt)', + 'No qualifying code changes — PR only touches docs, config, or assets', ); } return qualifying;