@zibby/skills 2.0.20 → 2.0.22
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/code-scan.d.ts +20 -3
- package/dist/code-scan.js +6 -3
- package/dist/index.js +95 -92
- package/dist/localWorkspace.js +6 -6
- package/dist/package.json +4 -4
- package/package.json +4 -4
package/dist/code-scan.d.ts
CHANGED
|
@@ -35,12 +35,22 @@
|
|
|
35
35
|
* `semgrep` CLI, no network, no telemetry, no registry) with a VENDORED curated
|
|
36
36
|
* ruleset + a generated local targets file; see resolveSemgrepBin. It scans ALL
|
|
37
37
|
* its languages in ONE invocation via a `-targets` file. JS/TS is intentionally
|
|
38
|
-
* left to oxlint (semgrep EXCLUDES it) to avoid double-scanning.
|
|
38
|
+
* left to oxlint (semgrep EXCLUDES it) to avoid double-scanning. Its 263 MB
|
|
39
|
+
* engine is the one thing here that is NOT in the image: it is materialized ON
|
|
40
|
+
* DEMAND from a sha256-pinned artifact on our own CDN the first time a scan
|
|
41
|
+
* needs it (see resolveSemgrepBin), so image size stops tracking how many
|
|
42
|
+
* engines the product supports.
|
|
39
43
|
* ruff (Python) + staticcheck (Go) remain SCAFFOLD entries (registry + parser
|
|
40
44
|
* present, clearly marked TODO) — semgrep now covers Python/Go for BREADTH; ruff/
|
|
41
45
|
* staticcheck can still be wired later for DEPTH. Best-effort throughout: a missing
|
|
42
46
|
* binary (spawn ENOENT), an unreadable file, or a parser hiccup NEVER throws — the
|
|
43
47
|
* scanner is skipped with a note and the others still run.
|
|
48
|
+
*
|
|
49
|
+
* ONE THING IS NOT BEST-EFFORT: an engine whose DELIVERY fails (download broke,
|
|
50
|
+
* sha256 mismatch, archive won't unpack). That is not "this stack has no linter",
|
|
51
|
+
* it is "the analysis you asked for silently did not happen", so it surfaces as
|
|
52
|
+
* `unavailable` on the scanner block plus a top-level `degraded` array — never as
|
|
53
|
+
* a skip. See runScanner.
|
|
44
54
|
*/
|
|
45
55
|
/**
|
|
46
56
|
* Build the semgrep-core `-targets` document for a set of RELATIVE file paths. This
|
|
@@ -78,12 +88,19 @@ export declare function parseOxlint(stdout: any): any;
|
|
|
78
88
|
* parse (stdout, stderr, code) => [{ file, line, severity, rule, message }]
|
|
79
89
|
* Adding a tool = ONE more entry here. NOTHING scanner-specific lives elsewhere.
|
|
80
90
|
*/
|
|
81
|
-
export declare const SCANNERS: {
|
|
91
|
+
export declare const SCANNERS: ({
|
|
82
92
|
id: string;
|
|
83
93
|
detect: (dir: any) => boolean;
|
|
84
94
|
langs: string[];
|
|
85
95
|
bin: () => string;
|
|
86
96
|
args: (files: any, ctx?: any) => any[];
|
|
87
97
|
parse: typeof parseOxlint;
|
|
88
|
-
}
|
|
98
|
+
} | {
|
|
99
|
+
id: string;
|
|
100
|
+
detect: (dir: any) => boolean;
|
|
101
|
+
langs: string[];
|
|
102
|
+
bin: () => Promise<any>;
|
|
103
|
+
args: (files: any, ctx?: any) => any[];
|
|
104
|
+
parse: typeof parseSemgrep;
|
|
105
|
+
})[];
|
|
89
106
|
export declare const codeScanSkill: any;
|
package/dist/code-scan.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import{spawnSync as
|
|
2
|
-
`)){let i=t.trim();if(!i)continue;let r;try{r=JSON.parse(i)}catch{continue}if(!r||typeof r!="object")continue;let
|
|
1
|
+
import{spawnSync as A}from"node:child_process";import{existsSync as u,readdirSync as _,statSync as N,writeFileSync as v,mkdirSync as b}from"node:fs";import{dirname as S,extname as p,join as l,relative as P,resolve as g}from"node:path";import{tmpdir as w}from"node:os";import{fileURLToPath as I}from"node:url";import{SKILL_META as C}from"@zibby/skill-ids";import{binPath as F}from"@zibby/bin-oxlint";import{ensureBinPath as L}from"@zibby/bin-semgrep";function D(){if(process.env.OXLINT_BIN)return process.env.OXLINT_BIN;try{let e=F();if(e&&u(e))return e}catch{}return"oxlint"}async function B(){return process.env.SEMGREP_CORE_BIN?process.env.SEMGREP_CORE_BIN:L()}function J(){if(process.env.MCP_SKILL_PATH)return process.env.MCP_SKILL_PATH;let e=S(I(import.meta.url)),n=g(e,"..","bin","mcp-skill.mjs");return u(n)?n:null}var E=new Set(["node_modules",".git","dist","build","out","vendor","target",".venv","venv","__pycache__",".next",".turbo","coverage",".zibby"]),G=400,$={plugins:["react","typescript","unicorn","oxc"],categories:{correctness:"error",suspicious:"warn"},rules:{"react/react-in-jsx-scope":"off","react/jsx-max-depth":"off","react/no-array-index-key":"off","react/jsx-key":"error","react/no-unknown-property":"error","no-unused-vars":"warn",eqeqeq:"warn"}},M=[".oxlintrc.json",".oxlintrc","oxlint.json"],d=null;function z(){if(d&&u(d))return d;try{let e=l(w(),"zibby-code-scan");b(e,{recursive:!0});let n=l(e,"oxlintrc.curated.json");return v(n,JSON.stringify($),"utf-8"),d=n,n}catch{return null}}var k={".java":"java",".py":"python",".go":"go",".rb":"ruby",".php":"php"},x=Object.keys(k),j={rules:[{id:"zibby-java-command-injection",languages:["java"],severity:"ERROR",message:"Command execution (Runtime.exec / ProcessBuilder) \u2014 command injection risk if the argument is attacker-influenced. Validate/allow-list the input or avoid a shell.",patterns:[{"pattern-either":[{pattern:"Runtime.getRuntime().exec(...)"},{pattern:"new ProcessBuilder(...)"}]}]},{id:"zibby-python-subprocess-shell",languages:["python"],severity:"ERROR",message:"subprocess call with shell=True \u2014 command injection risk. Pass an argv list and shell=False.",pattern:"subprocess.$F(..., shell=True, ...)"},{id:"zibby-python-yaml-load",languages:["python"],severity:"WARNING",message:"yaml.load without a safe loader can instantiate arbitrary Python objects. Use yaml.safe_load.",pattern:"yaml.load(...)"},{id:"zibby-go-command-injection",languages:["go"],severity:"WARNING",message:"os/exec with a non-constant command \u2014 verify the value is not attacker-controlled (command injection).",pattern:"exec.Command($CMD, ...)"},{id:"zibby-ruby-command-injection",languages:["ruby"],severity:"ERROR",message:"Shell/eval execution (system / eval) \u2014 command injection risk if the argument is attacker-influenced.",patterns:[{"pattern-either":[{pattern:"system(...)"},{pattern:"eval(...)"}]}]},{id:"zibby-php-command-injection",languages:["php"],severity:"ERROR",message:"Shell/eval execution (system / exec / shell_exec) \u2014 command injection risk if the argument is attacker-influenced.",patterns:[{"pattern-either":[{pattern:"system(...);"},{pattern:"exec(...);"},{pattern:"shell_exec(...);"}]}]}]},H=[".semgrep.yml",".semgrep.yaml","semgrep.yml","semgrep.yaml"],f=null;function U(){if(f&&u(f))return f;try{let e=l(w(),"zibby-code-scan");b(e,{recursive:!0});let n=l(e,"semgrep.curated.rules.json");return v(n,JSON.stringify(j),"utf-8"),f=n,n}catch{return null}}function W(e){return H.map(s=>l(e,s)).find(s=>u(s))||U()}function X(e){let n=[];for(let s of Array.isArray(e)?e:[]){if(typeof s!="string"||!s)continue;let t=k[p(s).toLowerCase()];if(!t)continue;let i=s.replace(/\\/g,"/");n.push(["CodeTarget",{path:{fpath:i,ppath:`/${i.replace(/^\/+/,"")}`},analyzer:t,products:["sast"]}])}return["Targets",n]}var q=0;function K(e){let n=X(e),s=n[1].length,t=l(w(),"zibby-code-scan");b(t,{recursive:!0});let i=l(t,`semgrep.targets.${process.pid}.${q++}.json`);return v(i,JSON.stringify(n),"utf-8"),{path:i,count:s}}function R(e){let n=typeof e=="string"?e.toUpperCase():"";return n==="ERROR"?"error":n==="INFO"||n==="INVENTORY"||n==="EXPERIMENT"?"info":"warning"}var V=Object.fromEntries(j.rules.map(e=>[e.id,e.severity]));function Y(e,n){if(n)return R(n);let s=V[e];return s?R(s):"warning"}function Q(e){let n=String(e||""),s=n.indexOf("{");if(s<0)return[];let t;try{t=JSON.parse(n.slice(s))}catch{return[]}return(t&&Array.isArray(t.results)?t.results:[]).map(r=>{if(!r||typeof r!="object")return null;let a=r.start&&typeof r.start=="object"?r.start:{},c=r.extra&&typeof r.extra=="object"?r.extra:{};return{file:r.path||"",line:Number.isFinite(a.line)?a.line:"",severity:Y(r.check_id,c.severity),rule:r.check_id||"",message:(c.message||"").trim()}}).filter(r=>r&&(r.file||r.message))}function Z(e,n,s=4e3){let t=new Set(n.map(a=>a.toLowerCase())),i=[e],r=0;for(;i.length;){let a=i.pop(),c;try{c=_(a,{withFileTypes:!0})}catch{continue}for(let o of c){if(++r>s)return!1;if(o.isDirectory())!E.has(o.name)&&!o.name.startsWith(".")&&i.push(l(a,o.name));else if(o.isFile()&&t.has(p(o.name).toLowerCase()))return!0}}return!1}function ee(e){let n=String(e||"").trim();if(!n)return[];let s;try{s=JSON.parse(n)}catch{return[]}return(Array.isArray(s)?s:s&&Array.isArray(s.diagnostics)?s.diagnostics:[]).map(i=>{if(!i||typeof i!="object")return null;let r=Array.isArray(i.labels)&&i.labels.length?i.labels[0]:null,a=r&&r.span?r.span:null;return{file:i.filename||a&&a.filename||"",line:a&&Number.isFinite(a.line)?a.line:"",severity:i.severity||"warning",rule:i.code||"",message:i.message||""}}).filter(i=>i&&(i.file||i.message))}function te(e){let n=String(e||"").trim();if(!n)return[];let s;try{s=JSON.parse(n)}catch{return[]}return Array.isArray(s)?s.map(t=>t&&typeof t=="object"?{file:t.filename||"",line:t.location&&Number.isFinite(t.location.row)?t.location.row:"",severity:"warning",rule:t.code||"",message:t.message||""}:null).filter(t=>t&&(t.file||t.message)):[]}function ne(e){let n=String(e||"").trim();if(!n)return[];let s=[];for(let t of n.split(`
|
|
2
|
+
`)){let i=t.trim();if(!i)continue;let r;try{r=JSON.parse(i)}catch{continue}if(!r||typeof r!="object")continue;let a=r.location||{};s.push({file:a.file||"",line:Number.isFinite(a.line)?a.line:"",severity:r.severity||"warning",rule:r.code||"",message:r.message||""})}return s.filter(t=>t.file||t.message)}var re=[{id:"oxlint",detect:e=>u(l(e,"package.json")),langs:[".ts",".tsx",".js",".jsx",".mjs",".cjs"],bin:()=>D(),args:(e,n={})=>{let s=n.baseDir||".",i=M.some(a=>u(l(s,a)))?null:z();return["--format","json",...i?["--config",i]:[],...e]},parse:ee},{id:"semgrep",detect:e=>Z(e,x),langs:x,bin:()=>B(),args:(e,n={})=>{let s=n.baseDir||".",t=W(s),{path:i}=K(e);return[...t?["-rules",t]:[],"-targets",i,"-json"]},parse:Q},{id:"ruff",detect:e=>u(l(e,"pyproject.toml"))||u(l(e,"requirements.txt"))||u(l(e,"setup.py")),langs:[".py"],bin:()=>process.env.RUFF_BIN||"ruff",args:e=>["check","--output-format","json",...e],parse:te},{id:"staticcheck",detect:e=>u(l(e,"go.mod")),langs:[".go"],bin:()=>process.env.STATICCHECK_BIN||"staticcheck",args:e=>["-f","json",...e],parse:ne}];function se(e,n,s){let t=[],i=new Set(n.map(a=>a.toLowerCase())),r=[e];for(;r.length&&t.length<s;){let a=r.pop(),c;try{c=_(a,{withFileTypes:!0})}catch{continue}for(let o of c){if(t.length>=s)break;o.isDirectory()?!E.has(o.name)&&!o.name.startsWith(".")&&r.push(l(a,o.name)):o.isFile()&&i.has(p(o.name).toLowerCase())&&t.push(l(a,o.name))}}return t}async function ie(e,n,s){let t=s.map(c=>P(n,c)).filter(Boolean);if(!t.length)return{scanner:e.id,skipped:"no matching files"};let i;try{i=await e.bin()}catch(c){return c?.isDeliveryFailure?{scanner:e.id,unavailable:`${e.id} unavailable: ${c.reason==="download-failed"?"download failed":c.reason}`,reason:c.reason,detail:String(c.message||c),impact:`${t.length} ${[...new Set(t.map(o=>p(o).toLowerCase()))].sort().join("/")} file(s) were NOT statically analysed. Treat this review as INCOMPLETE for those files and say so.`}:{scanner:e.id,skipped:`binary not available (${c?.reason||"unknown"}): ${String(c?.message||c)}`}}let r=A(i,e.args(t,{baseDir:n}),{cwd:n,encoding:"utf-8",timeout:180*1e3,maxBuffer:32*1024*1024});if(r.error){let c=r.error.code==="ENOENT"?`binary not installed (${i})`:String(r.error.message||r.error);return{scanner:e.id,skipped:c}}let a=[];try{let c=e.parse(r.stdout,r.stderr,r.status);a=Array.isArray(c)?c.filter(Boolean):[]}catch{a=[]}return{scanner:e.id,filesScanned:t.length,findings:a}}var me={id:"code-scan",serverName:"code_scan",meta:C["code-scan"],allowedTools:["mcp__code_scan__*"],description:"Code scan \u2014 run the RIGHT deterministic linter/analyzer for a checked-out repo (auto-detects the stack: JS/TS\u2192oxlint; Java/Python/Go/Ruby/PHP\u2192semgrep) and return structured findings. Fully local; the code never leaves the box.",promptFragment:`## Code Scan (deterministic linter, auto-detects the stack)
|
|
3
3
|
After you've cloned the repo, call \`scan_code\` to get DETERMINISTIC linter
|
|
4
4
|
findings for WHATEVER stack this repo is \u2014 it auto-detects (JS/TS\u2192oxlint;
|
|
5
5
|
Java/Python/Go/Ruby/PHP\u2192semgrep) and runs the matching tool. Pass \`files\` (the changed files, ideal for
|
|
6
6
|
a review) or \`dir\` (a directory to scan). Findings are GROUND-TRUTH CANDIDATES:
|
|
7
7
|
triage them for THIS change, verify each in context (false positives exist \u2014
|
|
8
8
|
trace before asserting), fold noise, and turn the real ones into inline
|
|
9
|
-
suggestions. Don't hand-lint what the tool already covers, and don't re-run it
|
|
9
|
+
suggestions. Don't hand-lint what the tool already covers, and don't re-run it.
|
|
10
|
+
If the result has a \`degraded\` array, an engine failed to DOWNLOAD \u2014 those files
|
|
11
|
+
were not scanned at all. Say so in your review; never let a delivery failure read
|
|
12
|
+
as a clean bill of health.`,resolve(){let e=J();return e?{type:"stdio",command:"node",args:[e,"../dist/code-scan.js","codeScanSkill"],env:{},description:this.description}:{command:null,args:[],env:{},description:this.description}},async handleToolCall(e,n){if(e!=="scan_code")return JSON.stringify({error:`Unknown tool: ${e}`});try{let s=Array.isArray(n?.files)?n.files.filter(o=>typeof o=="string"&&o.trim()):null,t;if(n?.dir&&typeof n.dir=="string"?t=g(n.dir):s&&s.length?t=ae(s.map(o=>g(o))):t=process.cwd(),!u(t)||!N(t).isDirectory())return JSON.stringify({error:`dir does not exist or is not a directory: ${t}`});let i=s?s.map(o=>g(t,o)):null,r=[],a=0;for(let o of re){let m=!1;try{m=!!o.detect(t)}catch{m=!1}if(!m)continue;let O=new Set(o.langs.map(h=>h.toLowerCase())),T=i?i.filter(h=>O.has(p(h).toLowerCase())):se(t,o.langs,G),y=await ie(o,t,T);Array.isArray(y.findings)&&(a+=y.findings.length),r.push(y)}if(!r.length)return JSON.stringify({ok:!0,baseDir:t,scanners:[],totalFindings:0,note:"No known stack detected (no package.json / pyproject / go.mod \u2026). Review by hand."});let c=r.filter(o=>o.unavailable).map(o=>`${o.unavailable} \u2014 ${o.impact}`);return c.length?JSON.stringify({ok:!0,baseDir:t,totalFindings:a,degraded:c,scanners:r}):JSON.stringify({ok:!0,baseDir:t,totalFindings:a,scanners:r})}catch(s){return JSON.stringify({error:`scan_code failed: ${s.message}`})}},tools:[{name:"scan_code",description:"Run the right deterministic linter for a checked-out repo and return structured findings. Auto-detects the stack (JS/TS \u2192 oxlint; Java/Python/Go/Ruby/PHP \u2192 semgrep OSS) and runs each matching tool, scoped to files in its languages. Pass `files` (e.g. the changed files of the PR \u2014 recommended for a review) OR `dir` (a directory to scan). Returns { scanners: [ { scanner, findings: [ { file, line, severity, rule, message } ] } ] }. Findings are CANDIDATES \u2014 verify each in context before asserting. Best-effort: a stack whose linter is not installed is skipped with a note. If the result carries a `degraded` array, an engine that SHOULD have run could not be delivered \u2014 those languages were not analysed at all, so say the review is incomplete for them rather than implying they came back clean.",input_schema:{type:"object",properties:{dir:{type:"string",description:"Absolute path to the checked-out repo (or subdirectory) to scan. Defaults to the current working directory."},files:{type:"array",items:{type:"string"},description:"Explicit list of files to scan (paths relative to `dir`, or absolute). Best for a code review \u2014 pass the PR's changed files. When omitted, the whole `dir` is walked (bounded)."}}}}]};function ae(e){if(!e.length)return process.cwd();if(e.length===1)return S(e[0]);let n=e.map(r=>r.split("/")),s=n[0],t=[];for(let r=0;r<s.length;r++){let a=s[r];if(n.every(c=>c[r]===a))t.push(a);else break}let i=t.join("/");return i&&u(i)&&N(i).isDirectory()?i:S(e[0])}export{re as SCANNERS,X as buildSemgrepTargets,me as codeScanSkill,ee as parseOxlint,Q as parseSemgrep};
|