@akshay7273/skill-advisories 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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Akshay Kumar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,45 @@
1
+ # skill-advisories
2
+
3
+ Open advisory database for AI agent skills, plugins, and MCP servers — OSV-style, machine-readable threat data for the agent ecosystem.
4
+
5
+ Every advisory documents a malicious, vulnerable, or typosquatted agent skill, backed by at least one published public reference (vendor report, researcher writeup, or registry takedown).
6
+
7
+ ## The feed (public API)
8
+
9
+ Consume the database directly — no install needed:
10
+
11
+ - Full feed: `https://raw.githubusercontent.com/Akshay7273/skill-advisories/main/feed/feed.json`
12
+ - Fast lookup index (`ecosystem:name` → advisory ids): `https://raw.githubusercontent.com/Akshay7273/skill-advisories/main/feed/index.json`
13
+ - Advisory schema: [`schema/advisory.schema.json`](schema/advisory.schema.json)
14
+
15
+ ## CLI
16
+
17
+ ```
18
+
19
+ # Check specific skill names
20
+
21
+ npx @akshay7273/skill-advisories check omnicogg my-other-skill
22
+
23
+ # Scan installed skill directories (~/.claude/skills, ~/.openclaw/skills, ...)
24
+
25
+ npx @akshay7273/skill-advisories scan
26
+
27
+ # Scan a specific directory, machine-readable output
28
+
29
+ npx @akshay7273/skill-advisories scan ./skills --json
30
+
31
+ ```
32
+
33
+ Options: `--json` (machine-readable output), `--feed <url-or-path>` (alternate feed source).
34
+
35
+ Exit codes: `0` no advisories matched · `1` matches found (CI-friendly) · `2` usage or feed error.
36
+
37
+ ## Data integrity
38
+
39
+ 1. Every advisory cites at least one published public reference.
40
+ 2. No hearsay: submissions without a published report or reproducible evidence are not listed.
41
+ 3. Advisories can be withdrawn; disputes are handled via GitHub issues.
42
+
43
+ ## License
44
+
45
+ Code: MIT. Advisory data: CC-BY-4.0 — free to use with attribution.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,123 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ import pc from "picocolors";
4
+ import { DEFAULT_FEED_URL, loadFeed, matchNames } from "./lookup.js";
5
+ import { defaultSkillDirs, listInstalledSkills } from "./scan.js";
6
+ const VERSION = createRequire(import.meta.url)("../package.json").version;
7
+ const HELP = `skill-advisories ${VERSION} — open advisory database for AI agent skills
8
+
9
+ Usage:
10
+ skill-advisories check <name...> Check skill names against the advisory feed
11
+ skill-advisories scan [dir...] Scan installed skill directories (defaults to known locations)
12
+
13
+ Options:
14
+ --json Machine-readable JSON output
15
+ --feed <source> Feed URL or local file path (default: official feed)
16
+ --help, -h Show this help
17
+ --version, -v Show version
18
+
19
+ Exit codes: 0 = no advisories matched, 1 = matches found, 2 = usage or feed error`;
20
+ function fail(message) {
21
+ console.error(pc.red(`error: ${message}`));
22
+ process.exit(2);
23
+ }
24
+ function parseArgs(argv) {
25
+ const positionals = [];
26
+ let json = false;
27
+ let feed = DEFAULT_FEED_URL;
28
+ for (let i = 0; i < argv.length; i++) {
29
+ const arg = argv[i];
30
+ if (arg === "--json") {
31
+ json = true;
32
+ }
33
+ else if (arg === "--feed") {
34
+ i++;
35
+ const value = argv[i];
36
+ if (!value)
37
+ fail("--feed requires a value");
38
+ feed = value;
39
+ }
40
+ else if (arg === "--help" || arg === "-h") {
41
+ console.log(HELP);
42
+ process.exit(0);
43
+ }
44
+ else if (arg === "--version" || arg === "-v") {
45
+ console.log(VERSION);
46
+ process.exit(0);
47
+ }
48
+ else if (arg.startsWith("--")) {
49
+ fail(`unknown option "${arg}"`);
50
+ }
51
+ else {
52
+ positionals.push(arg);
53
+ }
54
+ }
55
+ const [command, ...rest] = positionals;
56
+ return { command, positionals: rest, json, feed };
57
+ }
58
+ async function loadFeedOrFail(source) {
59
+ try {
60
+ return await loadFeed(source);
61
+ }
62
+ catch (err) {
63
+ fail(err instanceof Error ? err.message : String(err));
64
+ }
65
+ }
66
+ function report(checked, matches, json) {
67
+ if (json) {
68
+ console.log(JSON.stringify({
69
+ checked,
70
+ matchCount: matches.length,
71
+ matches: matches.map((m) => ({
72
+ query: m.query,
73
+ id: m.advisory.id,
74
+ type: m.advisory.type,
75
+ severity: m.advisory.severity,
76
+ summary: m.advisory.summary,
77
+ references: m.advisory.references.map((r) => r.url),
78
+ })),
79
+ }, null, 2));
80
+ }
81
+ else if (matches.length === 0) {
82
+ console.log(pc.green(`\u2705 ${checked} skill(s) checked \u2014 no advisories matched`));
83
+ }
84
+ else {
85
+ console.log(pc.red(`\u274c ${matches.length} advisory match(es) across ${checked} skill(s) checked:`));
86
+ for (const m of matches) {
87
+ console.log(` ${pc.bold(m.query)} \u2192 ${m.advisory.id} [${m.advisory.severity}] ${m.advisory.summary}`);
88
+ for (const ref of m.advisory.references) {
89
+ console.log(` ${ref.url}`);
90
+ }
91
+ }
92
+ }
93
+ process.exitCode = matches.length > 0 ? 1 : 0;
94
+ }
95
+ const args = parseArgs(process.argv.slice(2));
96
+ if (!args.command) {
97
+ console.log(HELP);
98
+ process.exit(2);
99
+ }
100
+ if (args.command === "check") {
101
+ if (args.positionals.length === 0)
102
+ fail("check requires at least one skill name");
103
+ const feed = await loadFeedOrFail(args.feed);
104
+ report(args.positionals.length, matchNames(feed, args.positionals), args.json);
105
+ }
106
+ else if (args.command === "scan") {
107
+ const dirs = args.positionals.length > 0 ? args.positionals : defaultSkillDirs();
108
+ const installed = await listInstalledSkills(dirs);
109
+ const names = installed.flatMap((d) => d.names);
110
+ if (!args.json) {
111
+ for (const d of installed) {
112
+ console.log(pc.dim(`scanning ${d.dir} (${d.names.length} skills)`));
113
+ }
114
+ if (installed.length === 0) {
115
+ console.log(pc.yellow("no skill directories found"));
116
+ }
117
+ }
118
+ const feed = await loadFeedOrFail(args.feed);
119
+ report(names.length, matchNames(feed, names), args.json);
120
+ }
121
+ else {
122
+ fail(`unknown command "${args.command}"`);
123
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,18 @@
1
+ import { mkdir, writeFile } from "node:fs/promises";
2
+ import pc from "picocolors";
3
+ import { buildFeed } from "./compile.js";
4
+ import { loadAdvisories } from "./load.js";
5
+ import { validateAdvisories } from "./validate.js";
6
+ const dir = process.argv[2] ?? "advisories";
7
+ const loaded = await loadAdvisories(dir);
8
+ const problems = await validateAdvisories(loaded);
9
+ if (problems.length > 0) {
10
+ console.log(pc.red(`\u274c refusing to compile: ${problems.length} validation problem(s). Run: npm run validate`));
11
+ process.exit(1);
12
+ }
13
+ const { feed, index } = buildFeed(loaded.map((l) => l.advisory));
14
+ await mkdir("feed", { recursive: true });
15
+ await writeFile("feed/feed.json", JSON.stringify(feed, null, 2) + "\n", "utf8");
16
+ await writeFile("feed/index.json", JSON.stringify(index, null, 2) + "\n", "utf8");
17
+ console.log(pc.green(`\u2705 feed/feed.json written \u2014 ${feed.advisory_count} advisories (test entries excluded)`));
18
+ console.log(pc.green(`\u2705 feed/index.json written \u2014 ${Object.keys(index).length} artifact keys`));
@@ -0,0 +1,21 @@
1
+ import type { Advisory } from "./types.js";
2
+ export type Feed = {
3
+ schema_version: "1";
4
+ name: string;
5
+ source: string;
6
+ generated: string;
7
+ advisory_count: number;
8
+ advisories: Advisory[];
9
+ };
10
+ export type FeedIndex = Record<string, string[]>;
11
+ export declare const FEED_NAME = "skill-advisories";
12
+ export declare const FEED_SOURCE = "https://github.com/Akshay7273/skill-advisories";
13
+ /**
14
+ * Build the distributable feed and lookup index from validated advisories.
15
+ * Test advisories never enter the feed. Withdrawn advisories stay in the
16
+ * feed (with their withdrawn timestamp) but are removed from the index.
17
+ */
18
+ export declare function buildFeed(advisories: Advisory[], now?: Date): {
19
+ feed: Feed;
20
+ index: FeedIndex;
21
+ };
@@ -0,0 +1,34 @@
1
+ export const FEED_NAME = "skill-advisories";
2
+ export const FEED_SOURCE = "https://github.com/Akshay7273/skill-advisories";
3
+ /**
4
+ * Build the distributable feed and lookup index from validated advisories.
5
+ * Test advisories never enter the feed. Withdrawn advisories stay in the
6
+ * feed (with their withdrawn timestamp) but are removed from the index.
7
+ */
8
+ export function buildFeed(advisories, now = new Date()) {
9
+ const live = advisories
10
+ .filter((a) => a.type !== "test")
11
+ .sort((a, b) => a.id.localeCompare(b.id));
12
+ const index = {};
13
+ for (const adv of live) {
14
+ if (adv.withdrawn)
15
+ continue;
16
+ for (const artifact of adv.artifacts) {
17
+ const key = `${artifact.ecosystem}:${artifact.name.toLowerCase()}`;
18
+ if (!index[key])
19
+ index[key] = [];
20
+ index[key].push(adv.id);
21
+ }
22
+ }
23
+ return {
24
+ feed: {
25
+ schema_version: "1",
26
+ name: FEED_NAME,
27
+ source: FEED_SOURCE,
28
+ generated: now.toISOString(),
29
+ advisory_count: live.length,
30
+ advisories: live,
31
+ },
32
+ index,
33
+ };
34
+ }
package/dist/load.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import type { Advisory } from "./types.js";
2
+ export type LoadedAdvisory = {
3
+ file: string;
4
+ advisory: Advisory;
5
+ };
6
+ /**
7
+ * Load every .json advisory in a directory (sorted by filename).
8
+ * Throws on unparseable JSON.
9
+ */
10
+ export declare function loadAdvisories(dir: string): Promise<LoadedAdvisory[]>;
package/dist/load.js ADDED
@@ -0,0 +1,22 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ /**
4
+ * Load every .json advisory in a directory (sorted by filename).
5
+ * Throws on unparseable JSON.
6
+ */
7
+ export async function loadAdvisories(dir) {
8
+ const files = (await readdir(dir)).filter((f) => f.endsWith(".json")).sort();
9
+ const loaded = [];
10
+ for (const file of files) {
11
+ const raw = await readFile(join(dir, file), "utf8");
12
+ let advisory;
13
+ try {
14
+ advisory = JSON.parse(raw);
15
+ }
16
+ catch {
17
+ throw new Error(`${file}: not valid JSON`);
18
+ }
19
+ loaded.push({ file, advisory });
20
+ }
21
+ return loaded;
22
+ }
@@ -0,0 +1,15 @@
1
+ import type { Feed } from "./compile.js";
2
+ import type { Advisory } from "./types.js";
3
+ export declare const DEFAULT_FEED_URL = "https://raw.githubusercontent.com/Akshay7273/skill-advisories/main/feed/feed.json";
4
+ export type Match = {
5
+ query: string;
6
+ advisory: Advisory;
7
+ artifactNames: string[];
8
+ };
9
+ /** Load the advisory feed from a URL (http/https) or a local file path. */
10
+ export declare function loadFeed(source?: string): Promise<Feed>;
11
+ /**
12
+ * Find advisories whose artifacts match any of the given names
13
+ * (case-insensitive, across all ecosystems). Withdrawn advisories are skipped.
14
+ */
15
+ export declare function matchNames(feed: Feed, names: string[]): Match[];
package/dist/lookup.js ADDED
@@ -0,0 +1,33 @@
1
+ import { readFile } from "node:fs/promises";
2
+ export const DEFAULT_FEED_URL = "https://raw.githubusercontent.com/Akshay7273/skill-advisories/main/feed/feed.json";
3
+ /** Load the advisory feed from a URL (http/https) or a local file path. */
4
+ export async function loadFeed(source = DEFAULT_FEED_URL) {
5
+ if (source.startsWith("http://") || source.startsWith("https://")) {
6
+ const res = await fetch(source);
7
+ if (!res.ok)
8
+ throw new Error(`failed to fetch feed: HTTP ${res.status}`);
9
+ return (await res.json());
10
+ }
11
+ return JSON.parse(await readFile(source, "utf8"));
12
+ }
13
+ /**
14
+ * Find advisories whose artifacts match any of the given names
15
+ * (case-insensitive, across all ecosystems). Withdrawn advisories are skipped.
16
+ */
17
+ export function matchNames(feed, names) {
18
+ const matches = [];
19
+ for (const query of names) {
20
+ const q = query.toLowerCase();
21
+ for (const advisory of feed.advisories) {
22
+ if (advisory.withdrawn)
23
+ continue;
24
+ const hits = advisory.artifacts
25
+ .filter((a) => a.name.toLowerCase() === q)
26
+ .map((a) => a.name);
27
+ if (hits.length > 0) {
28
+ matches.push({ query, advisory, artifactNames: hits });
29
+ }
30
+ }
31
+ }
32
+ return matches;
33
+ }
package/dist/scan.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ /** Known agent skill install locations, relative to the home directory. */
2
+ export declare const KNOWN_SKILL_DIRS: string[];
3
+ export declare function defaultSkillDirs(): string[];
4
+ /**
5
+ * List installed skills (subdirectory names) in each existing directory.
6
+ * Missing or unreadable directories are silently skipped.
7
+ */
8
+ export declare function listInstalledSkills(dirs: string[]): Promise<Array<{
9
+ dir: string;
10
+ names: string[];
11
+ }>>;
package/dist/scan.js ADDED
@@ -0,0 +1,34 @@
1
+ import { readdir } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ /** Known agent skill install locations, relative to the home directory. */
5
+ export const KNOWN_SKILL_DIRS = [
6
+ ".claude/skills",
7
+ ".openclaw/skills",
8
+ ".clawdbot/skills",
9
+ ".moltbot/skills",
10
+ ];
11
+ export function defaultSkillDirs() {
12
+ return KNOWN_SKILL_DIRS.map((d) => join(homedir(), d));
13
+ }
14
+ /**
15
+ * List installed skills (subdirectory names) in each existing directory.
16
+ * Missing or unreadable directories are silently skipped.
17
+ */
18
+ export async function listInstalledSkills(dirs) {
19
+ const found = [];
20
+ for (const dir of dirs) {
21
+ try {
22
+ const entries = await readdir(dir, { withFileTypes: true });
23
+ const names = entries
24
+ .filter((e) => e.isDirectory())
25
+ .map((e) => e.name)
26
+ .sort();
27
+ found.push({ dir, names });
28
+ }
29
+ catch {
30
+ // directory doesn't exist or is unreadable; skip
31
+ }
32
+ }
33
+ return found;
34
+ }
@@ -0,0 +1,30 @@
1
+ export type AdvisoryType = "malicious" | "vulnerable" | "typosquat" | "compromised" | "test";
2
+ export type Ecosystem = "claude-skill" | "claude-plugin" | "clawhub" | "mcp-server" | "npm" | "pypi" | "vscode-extension" | "github-action";
3
+ export type Behavior = "credential-theft" | "data-exfiltration" | "backdoor" | "malware-dropper" | "prompt-injection" | "crypto-theft" | "spam" | "other";
4
+ export type Severity = "critical" | "high" | "medium" | "low";
5
+ export type Reference = {
6
+ type: "REPORT" | "ADVISORY" | "ARTICLE" | "WEB";
7
+ url: string;
8
+ };
9
+ export type Artifact = {
10
+ ecosystem: Ecosystem;
11
+ name: string;
12
+ publisher?: string;
13
+ versions?: string[];
14
+ sha256?: string[];
15
+ };
16
+ export type Advisory = {
17
+ schema_version: "1";
18
+ id: string;
19
+ type: AdvisoryType;
20
+ summary: string;
21
+ details?: string;
22
+ severity: Severity;
23
+ behaviors?: Behavior[];
24
+ artifacts: Artifact[];
25
+ references: Reference[];
26
+ credits?: string[];
27
+ published: string;
28
+ modified: string;
29
+ withdrawn?: string;
30
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,16 @@
1
+ import pc from "picocolors";
2
+ import { loadAdvisories } from "./load.js";
3
+ import { validateAdvisories } from "./validate.js";
4
+ const dir = process.argv[2] ?? "advisories";
5
+ const loaded = await loadAdvisories(dir);
6
+ const problems = await validateAdvisories(loaded);
7
+ if (problems.length === 0) {
8
+ console.log(pc.green(`\u2705 ${loaded.length} advisor${loaded.length === 1 ? "y" : "ies"} valid in "${dir}"`));
9
+ }
10
+ else {
11
+ console.log(pc.red(`\u274c ${problems.length} problem(s) in "${dir}":`));
12
+ for (const p of problems) {
13
+ console.log(` ${pc.bold(p.file)} \u2014 ${p.problem}`);
14
+ }
15
+ process.exitCode = 1;
16
+ }
@@ -0,0 +1,10 @@
1
+ import type { LoadedAdvisory } from "./load.js";
2
+ export type ValidationProblem = {
3
+ file: string;
4
+ problem: string;
5
+ };
6
+ /**
7
+ * Validate advisories against the JSON Schema, then enforce repo invariants:
8
+ * filename matches id, ids are unique, modified is not earlier than published.
9
+ */
10
+ export declare function validateAdvisories(loaded: LoadedAdvisory[]): Promise<ValidationProblem[]>;
@@ -0,0 +1,40 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import _Ajv2020 from "ajv/dist/2020.js";
3
+ import _addFormats from "ajv-formats";
4
+ // ajv and ajv-formats are CommonJS. Under module=NodeNext, TypeScript types
5
+ // their default import as the module namespace, while Node/tsx resolve it to
6
+ // the class/function at runtime. These casts bridge that gap - do not simplify.
7
+ const Ajv2020 = _Ajv2020;
8
+ const addFormats = _addFormats;
9
+ const SCHEMA_URL = new URL("../schema/advisory.schema.json", import.meta.url);
10
+ /**
11
+ * Validate advisories against the JSON Schema, then enforce repo invariants:
12
+ * filename matches id, ids are unique, modified is not earlier than published.
13
+ */
14
+ export async function validateAdvisories(loaded) {
15
+ const schema = JSON.parse(await readFile(SCHEMA_URL, "utf8"));
16
+ const ajv = new Ajv2020({ allErrors: true });
17
+ addFormats(ajv);
18
+ const validate = ajv.compile(schema);
19
+ const problems = [];
20
+ const seen = new Set();
21
+ for (const { file, advisory } of loaded) {
22
+ if (!validate(advisory)) {
23
+ for (const err of validate.errors ?? []) {
24
+ problems.push({ file, problem: `schema: ${err.instancePath || "/"} ${err.message}` });
25
+ }
26
+ continue;
27
+ }
28
+ if (file !== `${advisory.id}.json`) {
29
+ problems.push({ file, problem: `filename must be ${advisory.id}.json` });
30
+ }
31
+ if (seen.has(advisory.id)) {
32
+ problems.push({ file, problem: `duplicate id ${advisory.id}` });
33
+ }
34
+ seen.add(advisory.id);
35
+ if (Date.parse(advisory.modified) < Date.parse(advisory.published)) {
36
+ problems.push({ file, problem: "modified is earlier than published" });
37
+ }
38
+ }
39
+ return problems;
40
+ }