@nifrajs/link-safety-cli 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/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +167 -0
- package/dist/index.js.map +1 -0
- package/package.json +47 -0
- package/src/index.ts +198 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { createLinkSafetyScanner, parseCorpusJsonl } from "@nifrajs/link-safety";
|
|
3
|
+
import { createTypeSafeLinkClassifier } from "@nifrajs/link-safety-typesafe";
|
|
4
|
+
const DEFAULT_CORPUS_PATH = "fixtures/synthetic-corpus.jsonl";
|
|
5
|
+
const DEFAULT_FIXTURES_PATH = "fixtures/synthetic-evaluation.jsonl";
|
|
6
|
+
const BOOLEAN_FLAGS = new Set(["stdin", "typesafe", "include-input", "allow-act"]);
|
|
7
|
+
await main();
|
|
8
|
+
async function main() {
|
|
9
|
+
const [command = "help", ...rawArgs] = Bun.argv.slice(2);
|
|
10
|
+
if (command === "help" || command === "--help") {
|
|
11
|
+
printUsage();
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const flags = parseFlags(rawArgs);
|
|
15
|
+
if (command === "scan") {
|
|
16
|
+
await scanCommand(flags);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (command === "evaluate") {
|
|
20
|
+
await evaluateCommand(flags);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
throw new Error("unknown command");
|
|
24
|
+
}
|
|
25
|
+
async function scanCommand(flags) {
|
|
26
|
+
const scanner = await createScanner(flags);
|
|
27
|
+
const inputs = flags.booleans.has("stdin") || flags.positional.length === 0
|
|
28
|
+
? (await Bun.stdin.text()).split(/\r?\n/u).filter((line) => line.trim() !== "")
|
|
29
|
+
: flags.positional;
|
|
30
|
+
if (inputs.length === 0)
|
|
31
|
+
throw new Error("scan requires a URL or stdin input");
|
|
32
|
+
const policy = readPolicy(flags);
|
|
33
|
+
for (const input of inputs) {
|
|
34
|
+
const result = await scanner.scan(input, { policy });
|
|
35
|
+
console.log(JSON.stringify({
|
|
36
|
+
...(flags.booleans.has("include-input") ? { input } : {}),
|
|
37
|
+
result,
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function evaluateCommand(flags) {
|
|
42
|
+
const corpusPath = flags.values.get("corpus") ?? DEFAULT_CORPUS_PATH;
|
|
43
|
+
const fixturesPath = flags.values.get("fixtures") ?? DEFAULT_FIXTURES_PATH;
|
|
44
|
+
const corpus = parseCorpusJsonl(await Bun.file(corpusPath).text(), {
|
|
45
|
+
version: corpusPath,
|
|
46
|
+
});
|
|
47
|
+
const fixtures = parseFixtures(await Bun.file(fixturesPath).text());
|
|
48
|
+
const scanner = createLinkSafetyScanner({ corpus });
|
|
49
|
+
const counts = new Map();
|
|
50
|
+
let correct = 0;
|
|
51
|
+
for (const fixture of fixtures) {
|
|
52
|
+
const result = await scanner.scan(fixture.url);
|
|
53
|
+
const actual = result.ok ? result.verdict : "error";
|
|
54
|
+
const bucket = counts.get(actual) ?? { total: 0, correct: 0 };
|
|
55
|
+
bucket.total += 1;
|
|
56
|
+
if (actual === fixture.expected) {
|
|
57
|
+
correct += 1;
|
|
58
|
+
bucket.correct += 1;
|
|
59
|
+
}
|
|
60
|
+
counts.set(actual, bucket);
|
|
61
|
+
}
|
|
62
|
+
console.log(JSON.stringify({
|
|
63
|
+
total: fixtures.length,
|
|
64
|
+
correct,
|
|
65
|
+
accuracy: fixtures.length === 0 ? 0 : correct / fixtures.length,
|
|
66
|
+
byActualVerdict: Object.fromEntries(counts),
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
async function createScanner(flags) {
|
|
70
|
+
const corpusPath = flags.values.get("corpus");
|
|
71
|
+
const corpus = corpusPath === undefined
|
|
72
|
+
? undefined
|
|
73
|
+
: parseCorpusJsonl(await Bun.file(corpusPath).text(), { version: corpusPath });
|
|
74
|
+
const classifier = flags.booleans.has("typesafe")
|
|
75
|
+
? createTypeSafeLinkClassifier({
|
|
76
|
+
...(Bun.env.TYPESAFE_API_KEY === undefined
|
|
77
|
+
? {}
|
|
78
|
+
: { apiKey: Bun.env.TYPESAFE_API_KEY }),
|
|
79
|
+
...(Bun.env.TYPESAFE_MODEL === undefined
|
|
80
|
+
? {}
|
|
81
|
+
: { model: Bun.env.TYPESAFE_MODEL }),
|
|
82
|
+
...(Bun.env.TYPESAFE_BASE_URL === undefined
|
|
83
|
+
? {}
|
|
84
|
+
: { baseUrl: Bun.env.TYPESAFE_BASE_URL }),
|
|
85
|
+
})
|
|
86
|
+
: undefined;
|
|
87
|
+
return createLinkSafetyScanner({
|
|
88
|
+
...(corpus === undefined ? {} : { corpus }),
|
|
89
|
+
...(classifier === undefined ? {} : { classifier }),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function readPolicy(flags) {
|
|
93
|
+
const minimumConfidenceValue = flags.values.get("min-confidence");
|
|
94
|
+
if (minimumConfidenceValue === undefined) {
|
|
95
|
+
return { allowAct: flags.booleans.has("allow-act") };
|
|
96
|
+
}
|
|
97
|
+
const minimumConfidence = Number(minimumConfidenceValue);
|
|
98
|
+
if (!Number.isFinite(minimumConfidence)) {
|
|
99
|
+
throw new Error("--min-confidence must be numeric");
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
allowAct: flags.booleans.has("allow-act"),
|
|
103
|
+
minimumConfidence,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
function parseFixtures(input) {
|
|
107
|
+
const fixtures = [];
|
|
108
|
+
for (const [index, line] of input.split(/\r?\n/u).entries()) {
|
|
109
|
+
if (line.trim() === "")
|
|
110
|
+
continue;
|
|
111
|
+
let value;
|
|
112
|
+
try {
|
|
113
|
+
value = JSON.parse(line);
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
throw new Error(`fixture line ${index + 1} is invalid JSON`);
|
|
117
|
+
}
|
|
118
|
+
if (!isRecord(value))
|
|
119
|
+
throw new Error(`fixture line ${index + 1} is invalid`);
|
|
120
|
+
const id = value.id;
|
|
121
|
+
const url = value.url;
|
|
122
|
+
const expected = value.expected;
|
|
123
|
+
if (typeof id !== "string" || typeof url !== "string" || !isVerdict(expected)) {
|
|
124
|
+
throw new Error(`fixture line ${index + 1} is invalid`);
|
|
125
|
+
}
|
|
126
|
+
fixtures.push({ id, url, expected });
|
|
127
|
+
}
|
|
128
|
+
return Object.freeze(fixtures);
|
|
129
|
+
}
|
|
130
|
+
function parseFlags(args) {
|
|
131
|
+
const values = new Map();
|
|
132
|
+
const booleans = new Set();
|
|
133
|
+
const positional = [];
|
|
134
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
135
|
+
const arg = args[index];
|
|
136
|
+
if (arg === undefined)
|
|
137
|
+
continue;
|
|
138
|
+
if (arg.startsWith("--")) {
|
|
139
|
+
const name = arg.slice(2);
|
|
140
|
+
const next = args[index + 1];
|
|
141
|
+
if (!BOOLEAN_FLAGS.has(name) && next !== undefined && !next.startsWith("--")) {
|
|
142
|
+
values.set(name, next);
|
|
143
|
+
index += 1;
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
booleans.add(name);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
positional.push(arg);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return { values, booleans, positional };
|
|
154
|
+
}
|
|
155
|
+
function isVerdict(value) {
|
|
156
|
+
return (value === "benign" ||
|
|
157
|
+
value === "suspicious" ||
|
|
158
|
+
value === "malicious" ||
|
|
159
|
+
value === "unknown");
|
|
160
|
+
}
|
|
161
|
+
function isRecord(value) {
|
|
162
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
163
|
+
}
|
|
164
|
+
function printUsage() {
|
|
165
|
+
console.log(`Nifra Link Safety\n\nCommands:\n scan [url...] [--stdin] [--corpus path] [--typesafe] [--allow-act]\n evaluate [--corpus path] [--fixtures path]\n\nEnvironment for --typesafe:\n TYPESAFE_API_KEY, TYPESAFE_MODEL, optional TYPESAFE_BASE_URL\n`);
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nifrajs/link-safety-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "CLI for Nifra Link Safety scans and evaluations.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"nifra",
|
|
7
|
+
"url",
|
|
8
|
+
"security",
|
|
9
|
+
"cli",
|
|
10
|
+
"evaluation"
|
|
11
|
+
],
|
|
12
|
+
"type": "module",
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"module": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"bin": {
|
|
17
|
+
"nifra-link-safety": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"exports": {
|
|
20
|
+
".": {
|
|
21
|
+
"types": "./dist/index.d.ts",
|
|
22
|
+
"import": "./dist/index.js",
|
|
23
|
+
"default": "./dist/index.js"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"src"
|
|
29
|
+
],
|
|
30
|
+
"license": "MIT",
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"repository": {
|
|
35
|
+
"type": "git",
|
|
36
|
+
"url": "git+https://github.com/nifrajs/nifra-link-safety.git",
|
|
37
|
+
"directory": "packages/link-safety-cli"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@nifrajs/link-safety": "^0.1.0",
|
|
41
|
+
"@nifrajs/link-safety-typesafe": "^0.1.0"
|
|
42
|
+
},
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc -p tsconfig.build.json",
|
|
45
|
+
"test": "bun test"
|
|
46
|
+
}
|
|
47
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import type { LinkSafetyPolicy, LinkSafetyVerdict } from "@nifrajs/link-safety";
|
|
4
|
+
import { createLinkSafetyScanner, parseCorpusJsonl } from "@nifrajs/link-safety";
|
|
5
|
+
import { createTypeSafeLinkClassifier } from "@nifrajs/link-safety-typesafe";
|
|
6
|
+
|
|
7
|
+
const DEFAULT_CORPUS_PATH = "fixtures/synthetic-corpus.jsonl";
|
|
8
|
+
const DEFAULT_FIXTURES_PATH = "fixtures/synthetic-evaluation.jsonl";
|
|
9
|
+
const BOOLEAN_FLAGS = new Set(["stdin", "typesafe", "include-input", "allow-act"]);
|
|
10
|
+
|
|
11
|
+
interface CliFlags {
|
|
12
|
+
readonly values: ReadonlyMap<string, string>;
|
|
13
|
+
readonly booleans: ReadonlySet<string>;
|
|
14
|
+
readonly positional: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface EvaluationFixture {
|
|
18
|
+
readonly id: string;
|
|
19
|
+
readonly url: string;
|
|
20
|
+
readonly expected: LinkSafetyVerdict;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await main();
|
|
24
|
+
|
|
25
|
+
async function main(): Promise<void> {
|
|
26
|
+
const [command = "help", ...rawArgs] = Bun.argv.slice(2);
|
|
27
|
+
if (command === "help" || command === "--help") {
|
|
28
|
+
printUsage();
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const flags = parseFlags(rawArgs);
|
|
32
|
+
if (command === "scan") {
|
|
33
|
+
await scanCommand(flags);
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (command === "evaluate") {
|
|
37
|
+
await evaluateCommand(flags);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
throw new Error("unknown command");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function scanCommand(flags: CliFlags): Promise<void> {
|
|
44
|
+
const scanner = await createScanner(flags);
|
|
45
|
+
const inputs =
|
|
46
|
+
flags.booleans.has("stdin") || flags.positional.length === 0
|
|
47
|
+
? (await Bun.stdin.text()).split(/\r?\n/u).filter((line) => line.trim() !== "")
|
|
48
|
+
: flags.positional;
|
|
49
|
+
if (inputs.length === 0) throw new Error("scan requires a URL or stdin input");
|
|
50
|
+
const policy = readPolicy(flags);
|
|
51
|
+
for (const input of inputs) {
|
|
52
|
+
const result = await scanner.scan(input, { policy });
|
|
53
|
+
console.log(
|
|
54
|
+
JSON.stringify({
|
|
55
|
+
...(flags.booleans.has("include-input") ? { input } : {}),
|
|
56
|
+
result,
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function evaluateCommand(flags: CliFlags): Promise<void> {
|
|
63
|
+
const corpusPath = flags.values.get("corpus") ?? DEFAULT_CORPUS_PATH;
|
|
64
|
+
const fixturesPath = flags.values.get("fixtures") ?? DEFAULT_FIXTURES_PATH;
|
|
65
|
+
const corpus = parseCorpusJsonl(await Bun.file(corpusPath).text(), {
|
|
66
|
+
version: corpusPath,
|
|
67
|
+
});
|
|
68
|
+
const fixtures = parseFixtures(await Bun.file(fixturesPath).text());
|
|
69
|
+
const scanner = createLinkSafetyScanner({ corpus });
|
|
70
|
+
const counts = new Map<
|
|
71
|
+
LinkSafetyVerdict | "error",
|
|
72
|
+
{ total: number; correct: number }
|
|
73
|
+
>();
|
|
74
|
+
let correct = 0;
|
|
75
|
+
for (const fixture of fixtures) {
|
|
76
|
+
const result = await scanner.scan(fixture.url);
|
|
77
|
+
const actual = result.ok ? result.verdict : "error";
|
|
78
|
+
const bucket = counts.get(actual) ?? { total: 0, correct: 0 };
|
|
79
|
+
bucket.total += 1;
|
|
80
|
+
if (actual === fixture.expected) {
|
|
81
|
+
correct += 1;
|
|
82
|
+
bucket.correct += 1;
|
|
83
|
+
}
|
|
84
|
+
counts.set(actual, bucket);
|
|
85
|
+
}
|
|
86
|
+
console.log(
|
|
87
|
+
JSON.stringify({
|
|
88
|
+
total: fixtures.length,
|
|
89
|
+
correct,
|
|
90
|
+
accuracy: fixtures.length === 0 ? 0 : correct / fixtures.length,
|
|
91
|
+
byActualVerdict: Object.fromEntries(counts),
|
|
92
|
+
}),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function createScanner(flags: CliFlags) {
|
|
97
|
+
const corpusPath = flags.values.get("corpus");
|
|
98
|
+
const corpus =
|
|
99
|
+
corpusPath === undefined
|
|
100
|
+
? undefined
|
|
101
|
+
: parseCorpusJsonl(await Bun.file(corpusPath).text(), { version: corpusPath });
|
|
102
|
+
const classifier = flags.booleans.has("typesafe")
|
|
103
|
+
? createTypeSafeLinkClassifier({
|
|
104
|
+
...(Bun.env.TYPESAFE_API_KEY === undefined
|
|
105
|
+
? {}
|
|
106
|
+
: { apiKey: Bun.env.TYPESAFE_API_KEY }),
|
|
107
|
+
...(Bun.env.TYPESAFE_MODEL === undefined
|
|
108
|
+
? {}
|
|
109
|
+
: { model: Bun.env.TYPESAFE_MODEL }),
|
|
110
|
+
...(Bun.env.TYPESAFE_BASE_URL === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { baseUrl: Bun.env.TYPESAFE_BASE_URL }),
|
|
113
|
+
})
|
|
114
|
+
: undefined;
|
|
115
|
+
return createLinkSafetyScanner({
|
|
116
|
+
...(corpus === undefined ? {} : { corpus }),
|
|
117
|
+
...(classifier === undefined ? {} : { classifier }),
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function readPolicy(flags: CliFlags): LinkSafetyPolicy {
|
|
122
|
+
const minimumConfidenceValue = flags.values.get("min-confidence");
|
|
123
|
+
if (minimumConfidenceValue === undefined) {
|
|
124
|
+
return { allowAct: flags.booleans.has("allow-act") };
|
|
125
|
+
}
|
|
126
|
+
const minimumConfidence = Number(minimumConfidenceValue);
|
|
127
|
+
if (!Number.isFinite(minimumConfidence)) {
|
|
128
|
+
throw new Error("--min-confidence must be numeric");
|
|
129
|
+
}
|
|
130
|
+
return {
|
|
131
|
+
allowAct: flags.booleans.has("allow-act"),
|
|
132
|
+
minimumConfidence,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function parseFixtures(input: string): readonly EvaluationFixture[] {
|
|
137
|
+
const fixtures: EvaluationFixture[] = [];
|
|
138
|
+
for (const [index, line] of input.split(/\r?\n/u).entries()) {
|
|
139
|
+
if (line.trim() === "") continue;
|
|
140
|
+
let value: unknown;
|
|
141
|
+
try {
|
|
142
|
+
value = JSON.parse(line);
|
|
143
|
+
} catch {
|
|
144
|
+
throw new Error(`fixture line ${index + 1} is invalid JSON`);
|
|
145
|
+
}
|
|
146
|
+
if (!isRecord(value)) throw new Error(`fixture line ${index + 1} is invalid`);
|
|
147
|
+
const id = value.id;
|
|
148
|
+
const url = value.url;
|
|
149
|
+
const expected = value.expected;
|
|
150
|
+
if (typeof id !== "string" || typeof url !== "string" || !isVerdict(expected)) {
|
|
151
|
+
throw new Error(`fixture line ${index + 1} is invalid`);
|
|
152
|
+
}
|
|
153
|
+
fixtures.push({ id, url, expected });
|
|
154
|
+
}
|
|
155
|
+
return Object.freeze(fixtures);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function parseFlags(args: readonly string[]): CliFlags {
|
|
159
|
+
const values = new Map<string, string>();
|
|
160
|
+
const booleans = new Set<string>();
|
|
161
|
+
const positional: string[] = [];
|
|
162
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
163
|
+
const arg = args[index];
|
|
164
|
+
if (arg === undefined) continue;
|
|
165
|
+
if (arg.startsWith("--")) {
|
|
166
|
+
const name = arg.slice(2);
|
|
167
|
+
const next = args[index + 1];
|
|
168
|
+
if (!BOOLEAN_FLAGS.has(name) && next !== undefined && !next.startsWith("--")) {
|
|
169
|
+
values.set(name, next);
|
|
170
|
+
index += 1;
|
|
171
|
+
} else {
|
|
172
|
+
booleans.add(name);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
positional.push(arg);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { values, booleans, positional };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isVerdict(value: unknown): value is LinkSafetyVerdict {
|
|
182
|
+
return (
|
|
183
|
+
value === "benign" ||
|
|
184
|
+
value === "suspicious" ||
|
|
185
|
+
value === "malicious" ||
|
|
186
|
+
value === "unknown"
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
191
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function printUsage(): void {
|
|
195
|
+
console.log(
|
|
196
|
+
`Nifra Link Safety\n\nCommands:\n scan [url...] [--stdin] [--corpus path] [--typesafe] [--allow-act]\n evaluate [--corpus path] [--fixtures path]\n\nEnvironment for --typesafe:\n TYPESAFE_API_KEY, TYPESAFE_MODEL, optional TYPESAFE_BASE_URL\n`,
|
|
197
|
+
);
|
|
198
|
+
}
|