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/LICENSE +21 -0
- package/README.md +84 -0
- package/assets/signatures-snapshot.json +796 -0
- package/dist/chunk-FJUDM3F7.js +1914 -0
- package/dist/cli.js +153 -0
- package/dist/mcp-S44QVYNH.js +88 -0
- package/docs/agent-gating.md +71 -0
- package/hooks/flagrix-clone-gate.sh +41 -0
- package/package.json +60 -0
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
|
+
}
|