Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -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.
60 changes: 60 additions & 0 deletions agent-node/src/codeDiff.js
Original file line number Diff line number Diff line change
@@ -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;
}
20 changes: 4 additions & 16 deletions agent-node/src/verificationEngine.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ───────────────────────────────────────────────────────────
Expand All @@ -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 ─────────────────────────────────────────────
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)` };
}
Expand Down
19 changes: 7 additions & 12 deletions agent-node/src/verifyMilestone.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -14,6 +14,8 @@
* platform API keys required.
*/

import { isQualifyingCodeFile } from './codeDiff.js';

// ─── Custom Error ─────────────────────────────────────────────────────────────

export class VerificationError extends Error {
Expand All @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
Loading