flagrix 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js ADDED
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ EXIT,
4
+ describeSource,
5
+ exitCodeFor,
6
+ loadSignatures,
7
+ parseRepoTarget,
8
+ renderRepoResult,
9
+ renderUserResult,
10
+ resolveToken,
11
+ scanGitHubRepo,
12
+ scanGitHubUser,
13
+ wantJson
14
+ } from "./chunk-FJUDM3F7.js";
15
+
16
+ // src/cli.ts
17
+ import { parseArgs } from "util";
18
+
19
+ // src/commands/scan.ts
20
+ async function runScan(target, options) {
21
+ let repo;
22
+ try {
23
+ repo = parseRepoTarget(target, options.ref);
24
+ } catch (error) {
25
+ console.error(`flagrix: ${error.message}`);
26
+ return EXIT.ERROR;
27
+ }
28
+ try {
29
+ const loaded = await loadSignatures();
30
+ const note = describeSource(loaded);
31
+ if (note) console.error(`flagrix: ${note}`);
32
+ const result = await scanGitHubRepo(repo, {
33
+ signatures: loaded.signatures,
34
+ githubToken: resolveToken(options.token)
35
+ });
36
+ if (wantJson(options.json)) {
37
+ console.log(JSON.stringify(result, null, 2));
38
+ } else {
39
+ console.log(renderRepoResult(result));
40
+ }
41
+ return exitCodeFor(result.riskLevel);
42
+ } catch (error) {
43
+ console.error(`flagrix: scan failed \u2014 ${error.message}`);
44
+ return EXIT.ERROR;
45
+ }
46
+ }
47
+
48
+ // src/commands/scan-user.ts
49
+ async function runScanUser(login, options) {
50
+ const username = login.trim().replace(/^https?:\/\/(www\.)?github\.com\//, "").replace(/\/.*$/, "");
51
+ if (!/^[A-Za-z0-9-]+$/.test(username)) {
52
+ console.error(`flagrix: not a GitHub username: "${login}"`);
53
+ return EXIT.ERROR;
54
+ }
55
+ try {
56
+ const loaded = await loadSignatures();
57
+ const note = describeSource(loaded);
58
+ if (note) console.error(`flagrix: ${note}`);
59
+ const result = await scanGitHubUser(username, {
60
+ githubToken: resolveToken(options.token),
61
+ userProfileRules: loaded.signatures.userProfileRules
62
+ });
63
+ if (wantJson(options.json)) {
64
+ console.log(JSON.stringify(result, null, 2));
65
+ } else {
66
+ console.log(renderUserResult(result));
67
+ }
68
+ return exitCodeFor(result.riskLevel);
69
+ } catch (error) {
70
+ console.error(`flagrix: profile scan failed \u2014 ${error.message}`);
71
+ return EXIT.ERROR;
72
+ }
73
+ }
74
+
75
+ // src/cli.ts
76
+ var VERSION = "0.1.0";
77
+ var HELP = `flagrix \u2014 scan GitHub repos and profiles for malware before you clone
78
+
79
+ Usage:
80
+ flagrix scan <url | owner/repo> [--ref <branch|sha>] [--json] [--token <pat>]
81
+ flagrix scan-user <username> [--json] [--token <pat>]
82
+ flagrix mcp start the MCP server (stdio) for agents
83
+
84
+ Options:
85
+ --ref <ref> branch, tag, or commit to scan (default: default branch)
86
+ --json machine-readable output (automatic when stdout is piped)
87
+ --token <pat> GitHub token (or FLAGRIX_GITHUB_TOKEN / GITHUB_TOKEN env)
88
+ -h, --help show this help
89
+ -v, --version show version
90
+
91
+ Exit codes:
92
+ 0 low risk 2 medium risk \u2014 review before proceeding
93
+ 1 scan failed 3 high risk \u2014 do not clone
94
+
95
+ The verdict is pinned to the scanned commit SHA (in the JSON output as
96
+ "commitSha"). Scanning is fully local: the only network calls go to the
97
+ GitHub/npm APIs and the public detection-rules repository. No telemetry.
98
+ Docs: https://github.com/flagrix-io/flagrix-cli`;
99
+ async function main() {
100
+ const { values, positionals } = parseArgs({
101
+ allowPositionals: true,
102
+ options: {
103
+ ref: { type: "string" },
104
+ json: { type: "boolean" },
105
+ token: { type: "string" },
106
+ help: { type: "boolean", short: "h" },
107
+ version: { type: "boolean", short: "v" }
108
+ }
109
+ });
110
+ if (values.version) {
111
+ console.log(VERSION);
112
+ return EXIT.LOW;
113
+ }
114
+ const [command, target] = positionals;
115
+ if (values.help || !command) {
116
+ console.log(HELP);
117
+ return values.help ? EXIT.LOW : EXIT.ERROR;
118
+ }
119
+ switch (command) {
120
+ case "scan":
121
+ if (!target) {
122
+ console.error("flagrix: scan requires a repository URL or owner/repo\n");
123
+ console.error(HELP);
124
+ return EXIT.ERROR;
125
+ }
126
+ return runScan(target, { ref: values.ref, json: values.json, token: values.token });
127
+ case "scan-user":
128
+ if (!target) {
129
+ console.error("flagrix: scan-user requires a GitHub username\n");
130
+ console.error(HELP);
131
+ return EXIT.ERROR;
132
+ }
133
+ return runScanUser(target, { json: values.json, token: values.token });
134
+ case "mcp": {
135
+ const { runMcp } = await import("./mcp-S44QVYNH.js");
136
+ return runMcp();
137
+ }
138
+ default:
139
+ console.error(`flagrix: unknown command "${command}"
140
+ `);
141
+ console.error(HELP);
142
+ return EXIT.ERROR;
143
+ }
144
+ }
145
+ main().then(
146
+ (code) => {
147
+ process.exitCode = code;
148
+ },
149
+ (error) => {
150
+ console.error(`flagrix: ${error.message}`);
151
+ process.exitCode = EXIT.ERROR;
152
+ }
153
+ );
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ describeSource,
4
+ loadSignatures,
5
+ parseRepoTarget,
6
+ resolveToken,
7
+ scanGitHubRepo,
8
+ scanGitHubUser,
9
+ securityScore
10
+ } from "./chunk-FJUDM3F7.js";
11
+
12
+ // src/commands/mcp.ts
13
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
14
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
15
+ import { z } from "zod";
16
+ var GUIDANCE = {
17
+ low: "LOW risk: no blocking findings. Standard caution still applies.",
18
+ medium: "MEDIUM risk: do NOT clone, install, or execute this code until a human has reviewed the findings.",
19
+ high: "HIGH risk: do NOT clone, install, or execute this code. Treat the repository as malicious."
20
+ };
21
+ async function runMcp() {
22
+ const server = new McpServer({ name: "flagrix", version: "0.1.0" });
23
+ server.registerTool(
24
+ "scan_github_repo",
25
+ {
26
+ title: "Scan a GitHub repository for malware",
27
+ description: "Scans a GitHub repository for malicious patterns (backdoors, data exfiltration, supply-chain attacks, known-bad packages) BEFORE cloning or installing it. The verdict is pinned to the repository's current commit SHA. Call this before cloning any untrusted repository.",
28
+ inputSchema: {
29
+ target: z.string().describe("GitHub repository URL or owner/repo slug, e.g. https://github.com/acme/repo"),
30
+ ref: z.string().optional().describe("Branch, tag, or commit to scan (default: default branch)")
31
+ }
32
+ },
33
+ async ({ target, ref }) => {
34
+ const repo = parseRepoTarget(target, ref);
35
+ const loaded = await loadSignatures();
36
+ const result = await scanGitHubRepo(repo, {
37
+ signatures: loaded.signatures,
38
+ githubToken: resolveToken()
39
+ });
40
+ const note = describeSource(loaded);
41
+ const summary = [
42
+ `${result.riskLevel.toUpperCase()} risk \u2014 security score ${securityScore(result.riskScore)}/100 (verdict pinned to commit ${result.commitSha ?? "unknown"}).`,
43
+ GUIDANCE[result.riskLevel],
44
+ ...note ? [`Note: ${note}.`] : []
45
+ ].join(" ");
46
+ return {
47
+ content: [
48
+ { type: "text", text: summary },
49
+ { type: "text", text: JSON.stringify(result, null, 2) }
50
+ ],
51
+ isError: false
52
+ };
53
+ }
54
+ );
55
+ server.registerTool(
56
+ "scan_github_user",
57
+ {
58
+ title: "Scan a GitHub user profile for scam signals",
59
+ description: "Scores a GitHub user profile for throwaway-account and scam signals (account age, followers, activity, repository authenticity). Useful for vetting a 'recruiter' or 'client' before engaging with their repositories.",
60
+ inputSchema: {
61
+ username: z.string().describe("GitHub username or profile URL")
62
+ }
63
+ },
64
+ async ({ username }) => {
65
+ const login = username.trim().replace(/^https?:\/\/(www\.)?github\.com\//, "").replace(/\/.*$/, "");
66
+ const loaded = await loadSignatures();
67
+ const result = await scanGitHubUser(login, {
68
+ githubToken: resolveToken(),
69
+ userProfileRules: loaded.signatures.userProfileRules
70
+ });
71
+ const summary = `${result.riskLevel.toUpperCase()} risk profile \u2014 security score ${securityScore(result.riskScore)}/100. ${result.recommendation}`;
72
+ return {
73
+ content: [
74
+ { type: "text", text: summary },
75
+ { type: "text", text: JSON.stringify(result, null, 2) }
76
+ ],
77
+ isError: false
78
+ };
79
+ }
80
+ );
81
+ await server.connect(new StdioServerTransport());
82
+ await new Promise(() => {
83
+ });
84
+ return 0;
85
+ }
86
+ export {
87
+ runMcp
88
+ };
@@ -0,0 +1,71 @@
1
+ # Gating agents with Flagrix
2
+
3
+ Two ways to put Flagrix between an AI agent and untrusted code: the **MCP server**
4
+ (the agent decides to scan, or is instructed to) and the **Claude Code hook**
5
+ (every `git clone` is gated, no cooperation needed).
6
+
7
+ ## Exit codes (the contract)
8
+
9
+ | code | meaning |
10
+ |---|---|
11
+ | 0 | low risk |
12
+ | 1 | scan failed (network, auth, bad target) |
13
+ | 2 | medium risk — review before proceeding |
14
+ | 3 | high risk — do not clone |
15
+
16
+ `--json` (or any piped stdout) emits the full scan result, including `commitSha` —
17
+ the verdict applies to that commit, not to whatever the branch points at later.
18
+
19
+ ## MCP server
20
+
21
+ ```bash
22
+ claude mcp add flagrix -- npx -y flagrix mcp
23
+ ```
24
+
25
+ Tools exposed: `scan_github_repo {target, ref?}` and `scan_github_user {username}`.
26
+ Set `GITHUB_TOKEN` in the environment to raise rate limits / scan private repos.
27
+
28
+ Suggested agent instruction (CLAUDE.md or system prompt):
29
+
30
+ > Before cloning or installing any repository you did not author, call the
31
+ > `scan_github_repo` tool and follow its guidance. Do not proceed on MEDIUM or
32
+ > HIGH verdicts without explicit human approval.
33
+
34
+ ## Claude Code clone gate (hook)
35
+
36
+ Copies of the hook script ship inside the npm package (`hooks/flagrix-clone-gate.sh`).
37
+
38
+ `~/.claude/settings.json` (or a project's `.claude/settings.json`):
39
+
40
+ ```json
41
+ {
42
+ "hooks": {
43
+ "PreToolUse": [
44
+ {
45
+ "matcher": "Bash",
46
+ "hooks": [
47
+ {
48
+ "type": "command",
49
+ "command": "bash \"$(npm root -g)/flagrix/hooks/flagrix-clone-gate.sh\""
50
+ }
51
+ ]
52
+ }
53
+ ]
54
+ }
55
+ }
56
+ ```
57
+
58
+ Install the package globally first (`npm i -g flagrix`), or vendor the script into
59
+ your repo and point the hook at it. The hook only reacts to `git clone` /
60
+ `gh repo clone` commands targeting github.com; everything else passes through.
61
+
62
+ ## CI
63
+
64
+ ```yaml
65
+ - name: Gate on Flagrix scan
66
+ run: npx -y flagrix scan ${{ github.event.pull_request.head.repo.html_url }}
67
+ env:
68
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69
+ ```
70
+
71
+ A non-zero exit fails the job (2/3 = risk verdicts, 1 = scan error).
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env bash
2
+ # Flagrix clone gate — Claude Code PreToolUse hook for the Bash tool.
3
+ #
4
+ # Scans any GitHub repo about to be cloned; blocks the clone (hook exit 2)
5
+ # when Flagrix rates it MEDIUM or HIGH risk. See docs/agent-gating.md for the
6
+ # settings.json wiring.
7
+ set -euo pipefail
8
+
9
+ payload="$(cat)"
10
+
11
+ # Extract a github.com repo URL from a `git clone`/`gh repo clone` command.
12
+ url="$(node -e '
13
+ let input = ""
14
+ process.stdin.on("data", (d) => (input += d))
15
+ process.stdin.on("end", () => {
16
+ try {
17
+ const cmd = JSON.parse(input)?.tool_input?.command ?? ""
18
+ if (!/\b(git\s+clone|gh\s+repo\s+clone)\b/.test(cmd)) return
19
+ const m =
20
+ cmd.match(/https?:\/\/github\.com\/[\w.-]+\/[\w.-]+/) ||
21
+ cmd.match(/git@github\.com:[\w.-]+\/[\w.-]+/) ||
22
+ cmd.match(/\bclone\s+([\w.-]+\/[\w.-]+)(?:\s|$)/)?.slice(1)
23
+ if (m) process.stdout.write(String(Array.isArray(m) ? m[0] : m))
24
+ } catch {}
25
+ })
26
+ ' <<< "$payload")"
27
+
28
+ # Not a GitHub clone — allow.
29
+ [ -z "$url" ] && exit 0
30
+
31
+ if npx -y flagrix scan "$url" --json > /dev/null 2>&1; then
32
+ exit 0 # low risk
33
+ else
34
+ code=$?
35
+ case "$code" in
36
+ 2) echo "Flagrix: MEDIUM risk — review https://github.com/flagrix-io/flagrix-cli#exit-codes before cloning $url" >&2 ;;
37
+ 3) echo "Flagrix: HIGH risk — do NOT clone $url (malicious patterns detected). Run: npx flagrix scan $url" >&2 ;;
38
+ *) echo "Flagrix: scan of $url failed (exit $code) — blocking out of caution. Run: npx flagrix scan $url" >&2 ;;
39
+ esac
40
+ exit 2 # exit 2 = block the tool call in Claude Code
41
+ fi
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "flagrix",
3
+ "version": "0.1.0",
4
+ "description": "Scan GitHub repos and profiles for malware before you clone — CLI and MCP server for the Flagrix scanner",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "bin": {
8
+ "flagrix": "dist/cli.js"
9
+ },
10
+ "files": [
11
+ "dist",
12
+ "assets",
13
+ "hooks",
14
+ "docs",
15
+ "README.md",
16
+ "LICENSE"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18.17"
20
+ },
21
+ "publishConfig": {
22
+ "access": "public"
23
+ },
24
+ "keywords": [
25
+ "security",
26
+ "malware-detection",
27
+ "supply-chain-security",
28
+ "scam-detection",
29
+ "github",
30
+ "scanner",
31
+ "cli",
32
+ "mcp",
33
+ "mcp-server"
34
+ ],
35
+ "homepage": "https://flagrix.io",
36
+ "mcpName": "io.github.flagrix-io/flagrix",
37
+ "repository": {
38
+ "type": "git",
39
+ "url": "git+https://github.com/flagrix-io/flagrix-cli.git"
40
+ },
41
+ "scripts": {
42
+ "build": "tsup",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "typecheck": "tsc --noEmit",
46
+ "sync-signatures": "node scripts/sync-signatures.mjs",
47
+ "prepublishOnly": "npm run sync-signatures && npm run build && npm test"
48
+ },
49
+ "dependencies": {
50
+ "@modelcontextprotocol/sdk": "^1.12.0",
51
+ "zod": "^3.24.0"
52
+ },
53
+ "devDependencies": {
54
+ "@flagrix/scanner-core": "github:flagrix-io/flagrix-scanner-core",
55
+ "@types/node": "^20.0.0",
56
+ "tsup": "^8.0.0",
57
+ "typescript": "^5.0.0",
58
+ "vitest": "^1.6.0"
59
+ }
60
+ }