cyber-skills 0.0.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.
@@ -0,0 +1,495 @@
1
+ #!/usr/bin/env node
2
+ import * as fs from 'node:fs'
3
+ import * as path from 'node:path'
4
+ import { pathToFileURL } from 'node:url'
5
+
6
+ type Severity = 'CRITICAL' | 'HIGH' | 'MEDIUM' | 'LOW'
7
+
8
+ interface Finding {
9
+ severity: Severity
10
+ checkId: string
11
+ name: string
12
+ evidence: string
13
+ fix: string
14
+ }
15
+
16
+ interface CheckResult {
17
+ criticals: Finding[]
18
+ warnings: Finding[]
19
+ }
20
+
21
+ const SKILL_DIRS = ['skills', '.agents/skills']
22
+
23
+ const GENERIC_PHRASES = [
24
+ 'helps with',
25
+ 'general purpose',
26
+ 'handles tasks',
27
+ 'use this skill when the user asks anything',
28
+ 'does things',
29
+ ]
30
+
31
+ const E1_PATTERNS: RegExp[] = [
32
+ /rm\s+-[rRf]*f[rRf]*\s+/,
33
+ /sudo\s+rm/,
34
+ /curl[^|\n]*\|\s*(ba)?sh/,
35
+ /wget[^|\n]*\|\s*(ba)?sh/,
36
+ /\bdd\s+if=/,
37
+ /\b(mkfs|fdisk|parted)\b/,
38
+ /kill\s+-9\s+1\b/,
39
+ /:\(\)\{\s*:\|:&\s*\}/,
40
+ /chmod\s+-R\s+777\s+/,
41
+ ]
42
+
43
+ const E2_PATTERNS: RegExp[] = [
44
+ /[Ii]gnore (previous|all|prior) instructions/,
45
+ /[Yy]ou are now [A-Z][a-zA-Z]/,
46
+ /[Ff]rom now on you are /,
47
+ /[Dd]isregard your (guidelines|rules)/,
48
+ /[Ff]orget your (guidelines|training)/,
49
+ /[Yy]our new instructions are/,
50
+ ]
51
+
52
+ // ── Helpers ─────────────────────────────────────────────────────────────────
53
+
54
+ function findSkillFiles(dirs: string[], cwd: string): string[] {
55
+ const seen = new Set<string>()
56
+ const results: string[] = []
57
+
58
+ for (const dir of dirs) {
59
+ const base = path.join(cwd, dir)
60
+ if (!fs.existsSync(base)) continue
61
+
62
+ for (const entry of fs.readdirSync(base, { withFileTypes: true })) {
63
+ const skillFile = path.join(base, entry.name, 'SKILL.md')
64
+ if (!fs.existsSync(skillFile)) continue
65
+ try {
66
+ const real = fs.realpathSync(skillFile)
67
+ if (!seen.has(real)) {
68
+ seen.add(real)
69
+ results.push(real)
70
+ }
71
+ } catch {
72
+ // skip unresolvable symlinks
73
+ }
74
+ }
75
+ }
76
+
77
+ return results.sort()
78
+ }
79
+
80
+ function parseFrontmatter(content: string): { name: string; description: string } {
81
+ const lines = content.split('\n')
82
+ let fmCount = 0
83
+ let name = ''
84
+ let description = ''
85
+
86
+ for (const line of lines) {
87
+ if (line.trim() === '---') {
88
+ fmCount++
89
+ if (fmCount === 2) break
90
+ continue
91
+ }
92
+ if (fmCount !== 1) continue
93
+
94
+ const nameMatch = line.match(/^name:\s*(.+)/)
95
+ if (nameMatch) name = nameMatch[1].trim().replace(/^["']|["']$/g, '')
96
+
97
+ const descMatch = line.match(/^description:\s*(.+)/)
98
+ if (descMatch) description = descMatch[1].trim().replace(/^["']|["']$/g, '')
99
+ }
100
+
101
+ return { name, description }
102
+ }
103
+
104
+ function extractBody(content: string): string {
105
+ const lines = content.split('\n')
106
+ let fmCount = 0
107
+ const bodyLines: string[] = []
108
+
109
+ for (const line of lines) {
110
+ if (line.trim() === '---') {
111
+ fmCount++
112
+ continue
113
+ }
114
+ if (fmCount >= 2) bodyLines.push(line)
115
+ }
116
+
117
+ return bodyLines.join('\n')
118
+ }
119
+
120
+ function extractCodeBlocks(content: string): string {
121
+ const lines = content.split('\n')
122
+ let inBlock = false
123
+ const out: string[] = []
124
+
125
+ for (let i = 0; i < lines.length; i++) {
126
+ if (lines[i].startsWith('```')) {
127
+ inBlock = !inBlock
128
+ continue
129
+ }
130
+ if (inBlock) out.push(`${i + 1}: ${lines[i]}`)
131
+ }
132
+
133
+ return out.join('\n')
134
+ }
135
+
136
+ // Removes inline `backtick` spans and "double-quoted" strings so that
137
+ // documentation examples don't trigger security/injection checks.
138
+ function stripExamples(content: string): string {
139
+ return content.replace(/`[^`]*`/g, '').replace(/"[^"]*"/g, '')
140
+ }
141
+
142
+ function isShellExpandedReference(source: string, matchIndex: number): boolean {
143
+ const previousChar = source[matchIndex - 1]
144
+ const previousTwoChars = source.slice(Math.max(0, matchIndex - 2), matchIndex)
145
+ return previousChar === '$' || previousTwoChars === ')/' || previousTwoChars === '}/'
146
+ }
147
+
148
+ // ── Check runner ─────────────────────────────────────────────────────────────
149
+
150
+ export function runChecks(filePath: string): CheckResult {
151
+ const criticals: Finding[] = []
152
+ const warnings: Finding[] = []
153
+
154
+ const crit = (checkId: string, name: string, evidence: string, fix: string) =>
155
+ criticals.push({ severity: 'CRITICAL', checkId, name, evidence, fix })
156
+
157
+ const warn = (severity: Severity, checkId: string, name: string, evidence: string, fix: string) =>
158
+ warnings.push({ severity, checkId, name, evidence, fix })
159
+
160
+ const content = fs.readFileSync(filePath, 'utf8')
161
+ const skillDir = path.dirname(filePath)
162
+ const dirName = path.basename(skillDir)
163
+ const parent = path.basename(path.dirname(skillDir))
164
+
165
+ const { name: fmName, description: fmDesc } = parseFrontmatter(content)
166
+ const body = extractBody(content)
167
+ const codeBlocks = extractCodeBlocks(content)
168
+ const stripped = stripExamples(content)
169
+
170
+ // ── Structure ───────────────────────────────────────────────────────────────
171
+
172
+ // S1: file must sit at <something>/skills/<name>/SKILL.md
173
+ if (parent !== 'skills') {
174
+ crit(
175
+ 'S1',
176
+ 'SKILL.md in own directory',
177
+ `path: ${filePath}`,
178
+ 'Move SKILL.md into its own named subdirectory under a skills/ directory',
179
+ )
180
+ }
181
+
182
+ // S2: name and description frontmatter required
183
+ if (!fmName) {
184
+ crit(
185
+ 'S2',
186
+ 'Required frontmatter: name',
187
+ 'name: field missing or empty',
188
+ `Add 'name: ${dirName}' to the YAML frontmatter block`,
189
+ )
190
+ }
191
+ if (!fmDesc) {
192
+ crit(
193
+ 'S2',
194
+ 'Required frontmatter: description',
195
+ 'description: field missing or empty',
196
+ 'Add a description: field to the YAML frontmatter block',
197
+ )
198
+ }
199
+
200
+ // S3: name must match directory name
201
+ if (fmName && fmName !== dirName) {
202
+ warn(
203
+ 'HIGH',
204
+ 'S3',
205
+ 'name matches directory',
206
+ `name: '${fmName}' but directory is '${dirName}'`,
207
+ `Set name: to '${dirName}'`,
208
+ )
209
+ }
210
+
211
+ // S4: relative file paths referenced in code blocks must exist inside the skill directory.
212
+ // Prose examples (e.g., scripts/setup.sh) are excluded — only fenced code blocks are checked.
213
+ // Skips: template placeholders (<>), globs (*), absolute/home paths.
214
+ const s4RefPattern = /([a-zA-Z0-9_.-]+\/[a-zA-Z0-9_./-]+\.[a-zA-Z]+)/g
215
+ // Skip repo-root-level prefixes (these are not skill-bundle references)
216
+ const s4Skip = /[<>*]|^(https?:\/\/|~|\/|skills\/|\.agents\/|\.github\/|\.cursor\/|\.vscode\/)/
217
+ const s4Ext = /\.(md|sh|mts|mjs|js|ts|py|json|yaml|yml)$/
218
+ const s4Refs = new Set<string>()
219
+ let m: RegExpExecArray | null
220
+ while ((m = s4RefPattern.exec(codeBlocks)) !== null) {
221
+ const ref = m[1]
222
+ if (isShellExpandedReference(codeBlocks, m.index)) continue
223
+ // Skip refs that are embedded within an absolute path (preceded by '/')
224
+ if (m.index > 0 && codeBlocks[m.index - 1] === '/') continue
225
+ if (!s4Skip.test(ref) && s4Ext.test(ref)) s4Refs.add(ref)
226
+ }
227
+ for (const ref of s4Refs) {
228
+ if (!fs.existsSync(path.join(skillDir, ref))) {
229
+ warn(
230
+ 'HIGH',
231
+ 'S4',
232
+ 'Referenced file does not exist in skill directory',
233
+ `${ref} (looked for ${path.join(skillDir, ref)})`,
234
+ 'Create the file inside the skill directory or remove the reference',
235
+ )
236
+ }
237
+ }
238
+
239
+ // S5: internal anchor links must resolve to real headings.
240
+ // Checked on stripped content so backtick-wrapped syntax examples don't trigger.
241
+ const anchorPattern = /\[([^\]]+)\]\(#([^)]+)\)/g
242
+ while ((m = anchorPattern.exec(stripped)) !== null) {
243
+ const anchor = m[2]
244
+ const headingPat = anchor.replace(/-/g, '[- ]')
245
+ if (!new RegExp(`^#{1,6}.*${headingPat}`, 'im').test(content)) {
246
+ warn(
247
+ 'MEDIUM',
248
+ 'S5',
249
+ 'Internal anchor link does not resolve',
250
+ `#${anchor}`,
251
+ `Add a heading matching '${anchor.replace(/-/g, ' ')}' or fix the link target`,
252
+ )
253
+ }
254
+ }
255
+
256
+ // ── Quality ──────────────────────────────────────────────────────────────────
257
+
258
+ // Q1: description must include trigger language
259
+ if (fmDesc && !/use this skill when|when to use/i.test(fmDesc)) {
260
+ warn(
261
+ 'HIGH',
262
+ 'Q1',
263
+ 'Trigger language in description',
264
+ `description: ${fmDesc}`,
265
+ "Add 'Use this skill when ...' to the description field",
266
+ )
267
+ }
268
+
269
+ // Q2: description must be specific (≥12 words, no generic phrases)
270
+ if (fmDesc) {
271
+ const wordCount = fmDesc.split(/\s+/).filter(Boolean).length
272
+ if (wordCount < 12) {
273
+ warn(
274
+ 'HIGH',
275
+ 'Q2',
276
+ 'Description too short (specificity)',
277
+ `${wordCount} words: ${fmDesc}`,
278
+ 'Expand the description to at least 12 words with specific trigger conditions',
279
+ )
280
+ }
281
+ const genericHit = GENERIC_PHRASES.find((p) => new RegExp(p, 'i').test(fmDesc))
282
+ if (genericHit) {
283
+ warn(
284
+ 'HIGH',
285
+ 'Q2',
286
+ 'Description uses vague generic phrase',
287
+ `matched '${genericHit}' in: ${fmDesc}`,
288
+ 'Replace with specific trigger conditions and outcomes',
289
+ )
290
+ }
291
+ }
292
+
293
+ // Q3: apparent sub-skills must carry "Internal skill:" prefix
294
+ if (fmDesc && /called by\b|^internal\b/i.test(fmDesc) && !/^Internal skill:/i.test(fmDesc)) {
295
+ warn(
296
+ 'MEDIUM',
297
+ 'Q3',
298
+ "Sub-skill missing 'Internal skill:' prefix",
299
+ `description: ${fmDesc}`,
300
+ "Prefix description with 'Internal skill: ' to prevent unintended activation",
301
+ )
302
+ }
303
+
304
+ // Q4: body must contain actionable content (numbered steps or headers)
305
+ if (!body.trim()) {
306
+ warn(
307
+ 'MEDIUM',
308
+ 'Q4',
309
+ 'No actionable instruction body',
310
+ 'body is empty after frontmatter',
311
+ 'Add numbered steps, decision logic, or ## section headers',
312
+ )
313
+ } else if (!/^\d+\.|^#{1,4} /m.test(body)) {
314
+ warn(
315
+ 'MEDIUM',
316
+ 'Q4',
317
+ 'No actionable instruction body',
318
+ 'body has no numbered steps or section headers',
319
+ 'Add numbered steps, decision logic, or ## section headers',
320
+ )
321
+ }
322
+
323
+ // Q5: description must be ≤120 characters
324
+ if (fmDesc && fmDesc.length > 120) {
325
+ warn(
326
+ 'MEDIUM',
327
+ 'Q5',
328
+ 'Description exceeds 120 characters',
329
+ `description: ${fmDesc.slice(0, 80)}… (${fmDesc.length} chars)`,
330
+ 'Trim to ≤120 chars; move example phrases to the skill body',
331
+ )
332
+ }
333
+
334
+ // Q10: SKILL.md must not treat stdout prose/tables as authoritative data
335
+ const q10Patterns: RegExp[] = [
336
+ /show (this |the )?(summary )?table/i,
337
+ /parse (the )?stdout/i,
338
+ /from the (script )?output/i,
339
+ /prints a summary table/i,
340
+ /print(s|ed)? a (summary )?table/i,
341
+ ]
342
+ const q10Line = body.split('\n').find((line) => {
343
+ if (/\b(do not|don't|never)\b/i.test(line) && /parse (the )?stdout/i.test(line)) return false
344
+ return q10Patterns.some((pat) => pat.test(line))
345
+ })
346
+ if (q10Line) {
347
+ warn(
348
+ 'HIGH',
349
+ 'Q10',
350
+ 'SKILL.md instructs parsing stdout prose/tables as data',
351
+ q10Line.trim(),
352
+ 'Point agents at an artifact file or jq on CLI output; stdout should be JSON ack only',
353
+ )
354
+ }
355
+
356
+ // Q11: interactive scripts must document --yes for agent runs
357
+ const scriptsDir = path.join(skillDir, 'scripts')
358
+ if (fs.existsSync(scriptsDir)) {
359
+ const scriptFiles = fs.readdirSync(scriptsDir).filter((f) => !f.startsWith('.'))
360
+ const hasInteractive = scriptFiles.some((f) => {
361
+ const src = fs.readFileSync(path.join(scriptsDir, f), 'utf8')
362
+ return /readline|createInterface|\.question\(/.test(src)
363
+ })
364
+ if (hasInteractive && !/--yes|-y/.test(content)) {
365
+ warn(
366
+ 'HIGH',
367
+ 'Q11',
368
+ 'Interactive script missing non-interactive agent path',
369
+ 'scripts/ uses readline/prompt but SKILL.md does not document --yes or -y',
370
+ 'Document --yes (or equivalent) in SKILL.md for autonomous agent runs',
371
+ )
372
+ }
373
+ }
374
+
375
+ // ── Security ─────────────────────────────────────────────────────────────────
376
+
377
+ // E1: dangerous shell commands — checked in fenced code blocks only.
378
+ // Code blocks are the only place an agent would execute commands.
379
+ for (const pat of E1_PATTERNS) {
380
+ const hit = codeBlocks.split('\n').find((l) => pat.test(l))
381
+ if (hit) {
382
+ crit(
383
+ 'E1',
384
+ 'Dangerous shell command (in code block)',
385
+ hit.trim(),
386
+ 'Remove or rewrite; never embed destructive commands in a skill',
387
+ )
388
+ break
389
+ }
390
+ }
391
+
392
+ // E2: prompt injection patterns — checked on prose with examples stripped.
393
+ // Inline backtick/quoted examples are documentation, not instructions.
394
+ for (const pat of E2_PATTERNS) {
395
+ const hit = stripped.split('\n').find((l) => pat.test(l))
396
+ if (hit) {
397
+ crit(
398
+ 'E2',
399
+ 'Prompt injection pattern',
400
+ hit.trim(),
401
+ 'Remove prompt-injection content; treat skill body as untrusted data',
402
+ )
403
+ break
404
+ }
405
+ }
406
+
407
+ // E6: force-push to main/master without an explicit confirmation step
408
+ const e6Hit = stripped.split('\n').find((l) => /git push.*(--force|-f )/.test(l) && /(main|master)/.test(l))
409
+ if (e6Hit) {
410
+ warn(
411
+ 'HIGH',
412
+ 'E6',
413
+ 'Silent permission escalation: force-push to main/master',
414
+ e6Hit.trim(),
415
+ 'Add an explicit user-confirmation step before the force-push',
416
+ )
417
+ }
418
+
419
+ return { criticals, warnings }
420
+ }
421
+
422
+ // ── Output ───────────────────────────────────────────────────────────────────
423
+
424
+ function printFinding(f: Finding): void {
425
+ const icon = f.severity === 'CRITICAL' ? '❌' : '⚠️ '
426
+ console.info(` ${icon} [${f.severity}] ${f.checkId} — ${f.name}`)
427
+ console.info(` Evidence: ${f.evidence}`)
428
+ console.info(` Fix: ${f.fix}`)
429
+ }
430
+
431
+ // ── Main ─────────────────────────────────────────────────────────────────────
432
+
433
+ function main(): void {
434
+ const args = process.argv.slice(2)
435
+ const pathIdx = args.indexOf('--path')
436
+ const targetPath = pathIdx !== -1 ? args[pathIdx + 1] : undefined
437
+
438
+ const cwd = process.cwd()
439
+ let skillFiles: string[]
440
+
441
+ if (targetPath) {
442
+ const resolved = path.resolve(cwd, targetPath)
443
+ const skillMd = resolved.endsWith('SKILL.md') ? resolved : path.join(resolved, 'SKILL.md')
444
+ if (!fs.existsSync(skillMd)) {
445
+ console.error(`No SKILL.md found at ${skillMd}`)
446
+ process.exit(1)
447
+ }
448
+ skillFiles = [fs.realpathSync(skillMd)]
449
+ } else {
450
+ skillFiles = findSkillFiles(SKILL_DIRS, cwd)
451
+ }
452
+
453
+ if (skillFiles.length === 0) {
454
+ console.info('No SKILL.md files found.')
455
+ process.exit(0)
456
+ }
457
+
458
+ console.info(`Validating ${skillFiles.length} skill(s)…`)
459
+
460
+ let totalCriticals = 0
461
+ let totalWarnings = 0
462
+
463
+ for (const filePath of skillFiles) {
464
+ const dirName = path.basename(path.dirname(filePath))
465
+ console.info(`\n── ${dirName} ─────────────────────────`)
466
+
467
+ const { criticals, warnings } = runChecks(filePath)
468
+ totalCriticals += criticals.length
469
+ totalWarnings += warnings.length
470
+
471
+ for (const f of criticals) printFinding(f)
472
+ for (const f of warnings) printFinding(f)
473
+
474
+ if (criticals.length === 0) {
475
+ console.info(' ✅ no CRITICAL findings')
476
+ } else {
477
+ console.info(' 🚨 DO NOT commit or install until all CRITICAL findings are resolved.')
478
+ }
479
+ }
480
+
481
+ console.info('\n══════════════════════════════════════')
482
+ console.info(`Results: ${totalCriticals} critical failure(s), ${totalWarnings} warning(s)`)
483
+
484
+ if (totalCriticals > 0) {
485
+ console.info('❌ Fix all CRITICAL findings before merging.')
486
+ process.exit(1)
487
+ }
488
+
489
+ console.info('✅ All checks passed (S1–S5, Q1–Q5, Q10–Q11, E1–E2, E6).')
490
+ console.info(' Run the audit-skill agent skill for full quality review (Q6–Q12, E3–E5, E7).')
491
+ }
492
+
493
+ if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) {
494
+ main()
495
+ }
@@ -0,0 +1,34 @@
1
+ ---
2
+ name: commit
3
+ description: "Use this skill when committing and you need Conventional Commits guidance — staging, messages, one concern per commit."
4
+ ---
5
+
6
+ # Commit
7
+
8
+ Minimal commit helper for repos using commit discipline. For full staging/splitting workflows, use `commit-work` from [softaworks/agent-toolkit](https://github.com/softaworks/agent-toolkit) instead.
9
+
10
+ ## Rules
11
+
12
+ - Commit every self-contained unit of work before moving on
13
+ - Use Conventional Commits: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`
14
+ - One concern per commit; never batch unrelated changes
15
+ - Use `git add -p` when a file has mixed changes
16
+ - Never commit with red tests; run the repo's validation command first
17
+
18
+ ## Workflow
19
+
20
+ 1. Inspect: `git status`, `git diff` (and `git diff --cached` after staging)
21
+ 2. Stage only what belongs: `git add -p`
22
+ 3. Review staged diff: `git diff --cached`
23
+ 4. Write message: `type: short imperative summary` (optional body for why)
24
+ 5. Commit: `git commit -m "type: summary"`
25
+
26
+ ## Message format
27
+
28
+ ```text
29
+ type: short summary in imperative mood
30
+
31
+ Optional body explaining what changed and why.
32
+ ```
33
+
34
+ Examples: `feat: add commit discipline hook registration`, `fix: resolve package root for run-hook`
@@ -0,0 +1,73 @@
1
+ ---
2
+ name: configure-awesome-sources
3
+ description: Use this skill when adding, removing, or inspecting the awesome-list sources used for curated skill discovery.
4
+ ---
5
+
6
+ # Configure Awesome Sources
7
+
8
+ Manage the layered source configuration used by `find-awesome-skill`.
9
+
10
+ ## Layers
11
+
12
+ Configuration files are loaded from:
13
+
14
+ 1. `.agents/awesome-skill-sources.local.json`
15
+ 2. `.agents/awesome-skill-sources.json`
16
+ 3. `~/.agents/awesome-skill-sources.json`
17
+
18
+ The local private file should stay gitignored.
19
+
20
+ Each file uses:
21
+
22
+ ```json
23
+ {
24
+ "version": 1,
25
+ "sources": [
26
+ { "repo": "owner/name", "path": "awesome-skills.json" }
27
+ ],
28
+ "disabled_sources": [
29
+ { "repo": "owner/name", "path": "awesome-skills.json" }
30
+ ]
31
+ }
32
+ ```
33
+
34
+ Use `disabled_sources` to suppress inherited sources. Do not use an `enabled` field.
35
+
36
+ ## Operations
37
+
38
+ List effective sources:
39
+
40
+ ```bash
41
+ npx tsx skills/configure-awesome-sources/scripts/configure-awesome-sources.mts list
42
+ ```
43
+
44
+ Add a source:
45
+
46
+ ```bash
47
+ npx tsx skills/configure-awesome-sources/scripts/configure-awesome-sources.mts add --layer repo --repo owner/name --path awesome-skills.json
48
+ ```
49
+
50
+ Disable an inherited source:
51
+
52
+ ```bash
53
+ npx tsx skills/configure-awesome-sources/scripts/configure-awesome-sources.mts disable --layer repo --repo owner/name --path awesome-skills.json
54
+ ```
55
+
56
+ Enable a disabled source:
57
+
58
+ ```bash
59
+ npx tsx skills/configure-awesome-sources/scripts/configure-awesome-sources.mts enable --layer repo --repo owner/name --path awesome-skills.json
60
+ ```
61
+
62
+ Remove a direct source:
63
+
64
+ ```bash
65
+ npx tsx skills/configure-awesome-sources/scripts/configure-awesome-sources.mts remove --layer repo --repo owner/name --path awesome-skills.json
66
+ ```
67
+
68
+ ## Guidance
69
+
70
+ - Use `local` for repo-private personal overrides.
71
+ - Use `repo` for team or company shared defaults in the current repository.
72
+ - Use `global` for user-wide defaults.
73
+ - If a repo-shared config disables a source, that suppression should win over a user-global source entry.