diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..05c7e5b --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,21 @@ +# Code ownership for security-sensitive paths +# These files require maintainer review on every PR + +# Security-critical: auth, secrets, OAuth flow +/src/auth-server.ts @patlux +/src/oauth.ts @patlux +/src/converters.ts @patlux + +# CI/CD pipeline +.github/workflows/ @patlux + +# Dependencies +package.json @patlux +package-lock.json @patlux + +# Security tooling +.semgrep/ @patlux +.gitleaks.toml @patlux + +# This file itself +.github/CODEOWNERS @patlux diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 06b6455..448a27f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,6 +8,9 @@ on: workflow_dispatch: jobs: + # ────────────────────────────────────────────────────────── + # Code correctness + # ────────────────────────────────────────────────────────── typecheck: runs-on: ubuntu-latest steps: @@ -29,3 +32,140 @@ jobs: cache: npm - run: npm ci - run: npm run format:check + + # ────────────────────────────────────────────────────────── + # Code-level vulnerability scanning (SAST) + # ────────────────────────────────────────────────────────── + codeql: + name: CodeQL SAST + runs-on: ubuntu-latest + permissions: + actions: read + contents: read + security-events: write + steps: + - uses: actions/checkout@v4 + - uses: github/codeql-action/init@v3 + with: + languages: javascript-typescript + queries: +security-extended,security-and-quality + - uses: github/codeql-action/autobuild@v3 + - uses: github/codeql-action/analyze@v3 + + # ────────────────────────────────────────────────────────── + # Custom static analysis — pi extension attack patterns + # ────────────────────────────────────────────────────────── + semgrep: + name: Semgrep — pi extension audit + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + container: + image: semgrep/semgrep:latest + steps: + - uses: actions/checkout@v4 + - name: Run Semgrep with custom rules + run: | + semgrep --config .semgrep/ --error --output semgrep-report.sarif --sarif . + - name: Upload SARIF + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: semgrep-report.sarif + category: semgrep-pi-audit + if: always() + + # ────────────────────────────────────────────────────────── + # Hardcoded secrets detection + # ────────────────────────────────────────────────────────── + gitleaks: + name: Gitleaks — secrets scan + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITLEAKS_ENABLE_COMMENTS: "true" + + # ────────────────────────────────────────────────────────── + # Dependency supply-chain security + # ────────────────────────────────────────────────────────── + deps-review: + name: Dependency review + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + permissions: + contents: read + pull-requests: write + steps: + - uses: actions/checkout@v4 + - name: Check if dependency graph is enabled + run: | + echo "Dependency review requires enabling Dependency graph in repo settings." + echo "Go to: https://github.com/patlux/pi-commandcode-provider/settings/security_analysis" + echo "Enable: Dependency graph" + - uses: actions/dependency-review-action@v4 + with: + fail-on-severity: high + allow-licenses: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, 0BSD + comment-summary-in-pr: always + continue-on-error: true + + deps-audit: + name: npm audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + cache: npm + - run: npm ci + - run: npm audit --audit-level=moderate + - name: Exit gracefully on audit findings + if: failure() + run: | + echo "::warning::npm audit found vulnerabilities. Review and patch before merging." + + # ────────────────────────────────────────────────────────── + # Postinstall script check — prevents install-time malware + # ────────────────────────────────────────────────────────── + check-scripts: + name: Check lifecycle scripts + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Check for malicious lifecycle scripts + run: | + echo "::group::package.json scripts" + node -e " + const pkg = require('./package.json'); + const dangerous = ['preinstall','install','postinstall','prepublish','prepare']; + const found = dangerous.filter(s => pkg.scripts && pkg.scripts[s]); + if (found.length) { + found.forEach(s => console.log('WARNING: package.json has "' + s + '":', pkg.scripts[s])); + process.exit(1); + } else { + console.log('No dangerous lifecycle scripts in package.json'); + } + " + echo "::endgroup::" + echo "::group::dependency scripts (top-level)" + npm query '.scripts' --all 2>/dev/null | node -e " + const d = require('fs').readFileSync('/dev/stdin','utf8'); + if (!d.trim()) { console.log('No dependency scripts found'); process.exit(0); } + let pkgs; + try { pkgs = JSON.parse(d); } catch(e) { console.log('Could not parse npm query output'); process.exit(0); } + if (!Array.isArray(pkgs)) pkgs = Object.values(pkgs); + const withScripts = pkgs.filter(p => p && p.pkgid && p.scripts); + withScripts.forEach(p => { + const dangerous = ['preinstall','install','postinstall','prepublish','prepare']; + const has = Object.keys(p.scripts || {}).filter(s => dangerous.includes(s)); + if (has.length) console.log('⚠', p.pkgid, 'has scripts:', Object.keys(p.scripts)); + }); + if (withScripts.length === 0) console.log('No dependency lifecycle scripts'); + " 2>&1 || true + echo "::endgroup::" diff --git a/.gitleaks.toml b/.gitleaks.toml new file mode 100644 index 0000000..fbc6932 --- /dev/null +++ b/.gitleaks.toml @@ -0,0 +1,51 @@ +# Custom Gitleaks config for pi-commandcode-provider. +# Extends the default rule set with patterns specific to pi extensions. + +title = "pi-commandcode-provider secret scan" + +[allowlist] + description = "Known safe paths and test fixtures" + paths = [ + # Test helpers with mock credentials + "tests/test-oauth.ts", + "tests/test-stream.ts", + "tests/test-pure-functions.ts", + "tests/test-pi-local.mjs", + "tests/test-omp-compat.mjs", + # CI workflows reference NPM_TOKEN secret name + ".github/workflows/publish.yml", + ] + +# ────────────────────────────────────────────────────────── +# Project-specific rules +# ────────────────────────────────────────────────────────── + +[[rules]] + id = "pi-commandcode-api-key" + description = "Command Code API key hardcoded in source" + regex = '''(?i)COMMANDCODE_API_KEY\s*[=:]\s*['"](user_[A-Za-z0-9_-]{10,}|cc_[A-Za-z0-9_-]{10,})['"]''' + tags = ["pi-extension", "commandcode", "api-key"] + +[[rules]] + id = "pi-auth-file-pattern" + description = "In-line pi auth.json content in source code" + regex = '''['"](apiKey|commandcode|command-code)['"]\s*:\s*['"]user_[A-Za-z0-9_-]{10,}['"]''' + tags = ["pi-extension", "auth"] + +[[rules]] + id = "pi-hardcoded-bearer-token" + description = "Hardcoded Bearer token (20+ chars) in Authorization header" + regex = '''Bearer [A-Za-z0-9_.\-]{20,}''' + tags = ["pi-extension", "auth-token"] + +[[rules]] + id = "pi-oauth-callback-url" + description = "OAuth callback URL with hardcoded key" + regex = '''callbackUrl\s*=\s*['"]http://localhost:\d+/callback['"]''' + tags = ["pi-extension", "oauth"] + +[[rules]] + id = "pi-test-api-key" + description = "Test API key value that looks real" + regex = '''(user_testKey|mock-key|fake-key|test-api-key)''' + tags = ["pi-extension", "test"] diff --git a/.semgrep/pi-extension-audit.yaml b/.semgrep/pi-extension-audit.yaml new file mode 100644 index 0000000..b56c540 --- /dev/null +++ b/.semgrep/pi-extension-audit.yaml @@ -0,0 +1,273 @@ +rules: + # ──────────────────────────────────────────────────────────────────────── + # Exfiltration: sending secrets to remote servers + # ──────────────────────────────────────────────────────────────────────── + + - id: pi-extension-data-exfiltration-fetch + patterns: + - pattern: fetch($URL, ...) + message: > + Data exfiltration risk: sending data to $URL via fetch. + Verify the destination is not an attacker-controlled server. + severity: ERROR + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + - "**/index.ts" + focus-metavariable: $URL + + - id: pi-extension-data-exfiltration-xmlhttprequest + pattern: new XMLHttpRequest() + message: > + Suspicious XMLHttpRequest usage in provider code. + Could exfiltrate data to external servers. + severity: WARNING + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + + # ──────────────────────────────────────────────────────────────────────── + # Secret leakage: logging / sending API keys + # ──────────────────────────────────────────────────────────────────────── + + - id: pi-extension-logging-secrets + patterns: + - pattern-either: + - pattern: console.log(...) + - pattern: console.error(...) + - pattern: console.warn(...) + - pattern-regex: "api[kK]ey|API_KEY|COMMANDCODE|Bearer|authPath" + message: > + Potential secret logging: log statement includes an API key, auth token, + or auth file path pattern. Do not log secrets. + severity: ERROR + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + - "**/index.ts" + + - id: pi-extension-secret-in-error-message + patterns: + - pattern: Error($MSG) + - pattern-regex: "api[kK]ey|API_KEY|COMMANDCODE|Bearer" + message: > + Possible secret leak in error message: $MSG. + Error messages surfaced to pi may leak credentials. + Use generic messages instead. + severity: WARNING + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + - "**/index.ts" + + # ──────────────────────────────────────────────────────────────────────── + # URL hijacking: changing API base endpoints + # ──────────────────────────────────────────────────────────────────────── + + - id: pi-extension-api-base-override + patterns: + - pattern-either: + - pattern: $VAR = "..." + - pattern: $VAR = process.env.$ENV_VAR ?? "..." + - metavariable-regex: + metavariable: $VAR + regex: "(?i).*(api_?base|api_base_url|base_url|host|endpoint|models_url).*" + - metavariable-regex: + metavariable: $ENV_VAR + regex: "(?i).*API.*BASE.*" + message: > + API base URL override: $ENV_VAR can redirect all requests including auth + headers to any server. Review any change to how this variable is set. + severity: ERROR + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + - "**/index.ts" + + + + # ──────────────────────────────────────────────────────────────────────── + # Supply chain: dependency injection + # ──────────────────────────────────────────────────────────────────────── + + - id: pi-extension-untrusted-dynamic-require + patterns: + - pattern: require($MODULE) + - metavariable-regex: + metavariable: $MODULE + regex: "^(?!['\"](\\.[/\\\\]|node:?|@earendil-works/))['\"]" + message: > + Dynamic require() of non-local, non-standard-library module: $MODULE. + External contributors could introduce malicious packages this way. + severity: ERROR + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + - "**/index.ts" + + - id: pi-extension-postinstall-script + patterns: + - pattern: '"postinstall": "$SCRIPT"' + message: > + postinstall script detected: $SCRIPT. Install-time scripts can execute + arbitrary code on user machines. Must be reviewed with extreme care. + severity: ERROR + languages: [json] + paths: + include: + - "package.json" + + # ──────────────────────────────────────────────────────────────────────── + # File system: reading auth files / ~/.pi + # ──────────────────────────────────────────────────────────────────────── + + - id: pi-extension-auth-path-traversal + patterns: + - pattern: join($HOME, $PATH) + - metavariable-regex: + metavariable: $HOME + regex: "homedir|homeDir|HOME|process\\.env\\.HOME" + - metavariable-regex: + metavariable: $PATH + regex: "['\"].*\\.\\./.*['\"]" + message: > + Auth path traversal: $HOME / $PATH could read outside the intended auth + directory. Path must stay within ~/.commandcode, ~/.pi/agent, ~/.omp/agent. + severity: ERROR + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + + - id: pi-extension-readfile-sensitive-paths + patterns: + - pattern: readFileSync($PATH, ...) + - pattern-not: readFileSync($PATH, "utf-8") + - metavariable-regex: + metavariable: $PATH + regex: "(auth\\.json|credentials|\\.env|id_rsa|ssh|\\.netrc|netrc|\\.npmrc)" + message: > + Suspicious file read: $PATH. Auth files or credentials being read with + potentially unsafe encoding. Review carefully. + severity: WARNING + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + + # ──────────────────────────────────────────────────────────────────────── + # Network: unexpected fetch calls + # ──────────────────────────────────────────────────────────────────────── + + - id: pi-extension-unexpected-fetch + pattern: fetch(...) + message: > + Direct fetch() call detected. Provider code should use injected + fetchImpl for testability and security. If this is intentional, + add a // nosemgrep comment on the line above. + severity: WARNING + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + - "**/index.ts" + + # ──────────────────────────────────────────────────────────────────────── + # Shell execution + # ──────────────────────────────────────────────────────────────────────── + + - id: pi-extension-shell-execution + patterns: + - pattern-either: + - pattern: child_process.exec(...) + - pattern: child_process.execSync(...) + - pattern: child_process.spawn(...) + - pattern: child_process.spawnSync(...) + - pattern: child_process.fork(...) + - pattern: exec($SCRIPT) + - pattern: execSync($SCRIPT) + message: > + Shell execution: $SCRIPT. External contributors should not add + child_process calls to provider source code. + severity: ERROR + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + - "**/index.ts" + + # ──────────────────────────────────────────────────────────────────────── + # Malicious import patterns + # ──────────────────────────────────────────────────────────────────────── + + - id: pi-extension-unusual-import + patterns: + - pattern-either: + - pattern: "import $X from \"...\"" + - pattern: "const $X = require(\"...\")" + - metavariable-regex: + metavariable: $X + regex: "(compression|pako|zlib|tar|stream|archiver|request|axios|needle|got|superagent|node-fetch|undici)" + message: > + Suspicious import of $X in provider source. Network or compression + libraries could be used for data exfiltration. + severity: WARNING + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + - "**/index.ts" + + - id: pi-extension-eval-like + patterns: + - pattern-either: + - pattern: eval(...) + - pattern: new Function(...) + - pattern: setTimeout($X, ...) + - pattern: setInterval($X, ...) + - pattern-not: setTimeout(() => ..., ...) + - pattern-not: setTimeout(function(...) {...}, ...) + - pattern-not: setTimeout(function $F(...) {...}, ...) + - pattern-not: setInterval(() => ..., ...) + - pattern-not: setInterval(function(...) {...}, ...) + - pattern-not: setInterval(function $F(...) {...}, ...) + message: > + Code injection risk: eval, Function constructor, or eval-like setTimeout + detected. These can execute arbitrary code. + severity: ERROR + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + - "**/index.ts" + + # ──────────────────────────────────────────────────────────────────────── + # Process environment: reading secrets + # ──────────────────────────────────────────────────────────────────────── + + - id: pi-extension-env-leak + patterns: + - pattern: process.env.$VAR + - metavariable-regex: + metavariable: $VAR + regex: "(?!COMMANDCODE_|NODE_|PATH|HOME|SHELL|USER|LANG|LC_|TERM|TMPDIR|NIX_).*" + message: > + Reading unexpected environment variable $VAR. Provider should only + read COMMANDCODE_* variables. + severity: WARNING + languages: [javascript, typescript] + paths: + include: + - "**/*.ts" + - "**/index.ts" + + # ──────────────────────────────────────────────────────────────────────── + # OAuth flow manipulation + # ──────────────────────────────────────────────────────────────────────── + diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 0000000..611fe76 --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,7 @@ +# Semgrep ignore patterns +# Exclude test files and build artifacts + +tests/ +node_modules/ +dist/ +coverage/