fullshield 2.4.0 → 3.1.1
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/CHANGELOG.md +21 -0
- package/README.md +19 -8
- package/dist/agent.js +45 -45
- package/dist/cli/index.js +113 -92
- package/dist/compliance.js +1 -1
- package/dist/index.d.ts +14 -0
- package/dist/index.js +73 -73
- package/dist/llm.js +2 -2
- package/dist/mcp.js +44 -44
- package/dist/supply.js +3 -3
- package/models/index.json +1 -1
- package/package.json +1 -1
- package/templates/action/action.yml +2 -2
- package/templates/fullshield-bumps.yml +17 -0
- package/templates/github-workflow-action.yml +1 -1
- package/templates/github-workflow-diff.yml +15 -0
package/dist/supply.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {existsSync,statSync,readFileSync,readdirSync}from'fs';import {join,relative}from'path';import {createHash}from'crypto';function S(t,e,n){let r=(n??"").replace(/\s+/g," ").trim();return createHash("sha256").update(`${t}\0${e??""}\0${r}`).digest("hex").slice(0,16)}function v(){return process.env.FULLSHIELD_EXPERIMENTAL_VEX==="1"}var y="registry.npmjs.org",I=["preinstall","install","postinstall"];function j(t){let e;try{e=new URL(t);}catch{throw new Error("supply: registry URL must be https://registry.npmjs.org/...")}if(e.protocol!=="https:"||e.hostname!==y)throw new Error(`supply: only ${y} is allowed (got ${e.hostname||t})`)}function C(t){if(t.startsWith("@")){let n=t.indexOf("/");if(n<0)return t;let r=t.slice(0,n),i=t.slice(n+1),s=i.includes("@")?i.slice(0,i.indexOf("@")):i;return `${r}/${s}`}let e=t.indexOf("@");return e>0?t.slice(0,e):t}function a(t,e,n,r,i="high"){return {id:S(t,r,n),ruleId:t,module:"deps",title:e,severity:i,confidence:"high",file:r,message:n,remediation:"Do not install this package until the behavior is reviewed. Use --ignore-scripts."}}function f(t,e=[]){if(!existsSync(t))return e;for(let n of readdirSync(t,{withFileTypes:true})){let r=join(t,n.name);if(n.isDirectory()){if(n.name==="node_modules")continue;f(r,e);}else n.isFile()&&/\.(js|cjs|mjs|ts)$/.test(n.name)&&e.push(r);}return e}function
|
|
1
|
+
import {existsSync,statSync,readFileSync,readdirSync}from'fs';import {join,relative}from'path';import {createHash}from'crypto';function S(t,e,n){let r=(n??"").replace(/\s+/g," ").trim();return createHash("sha256").update(`${t}\0${e??""}\0${r}`).digest("hex").slice(0,16)}function v(){return process.env.FULLSHIELD_EXPERIMENTAL_VEX==="1"}var y="registry.npmjs.org",I=["preinstall","install","postinstall"];function j(t){let e;try{e=new URL(t);}catch{throw new Error("supply: registry URL must be https://registry.npmjs.org/...")}if(e.protocol!=="https:"||e.hostname!==y)throw new Error(`supply: only ${y} is allowed (got ${e.hostname||t})`)}function C(t){if(t.startsWith("@")){let n=t.indexOf("/");if(n<0)return t;let r=t.slice(0,n),i=t.slice(n+1),s=i.includes("@")?i.slice(0,i.indexOf("@")):i;return `${r}/${s}`}let e=t.indexOf("@");return e>0?t.slice(0,e):t}function a(t,e,n,r,i="high"){return {id:S(t,r,n),ruleId:t,module:"deps",title:e,severity:i,confidence:"high",file:r,message:n,remediation:"Do not install this package until the behavior is reviewed. Use --ignore-scripts."}}function f(t,e=[]){if(!existsSync(t))return e;for(let n of readdirSync(t,{withFileTypes:true})){let r=join(t,n.name);if(n.isDirectory()){if(n.name==="node_modules")continue;f(r,e);}else n.isFile()&&/\.(js|cjs|mjs|ts)$/.test(n.name)&&e.push(r);}return e}function b(t){return JSON.parse(readFileSync(t,"utf8"))}function T(t){return t?I.filter(e=>typeof t[e]=="string"):[]}function $(t){let e=[],n=join(t,"package.json");if(!existsSync(n)||!statSync(n).isFile())return e;let i=b(n).scripts??{},s=T(i);s.length>0&&e.push(a("SUPPLY-INSTALLSCRIPT-001","Package declares an install script",`install scripts: ${s.join(", ")}`,"package.json"));let o=f(t).map(c=>({file:relative(t,c)||c,text:readFileSync(c,"utf8")})),m=o.some(c=>c.text.includes(".npmrc")||c.text.includes(".ssh")),h=o.some(c=>/https\.request|http\.request|fetch\s*\(|axios\./.test(c.text));if(m&&h){let c=o.find(p=>p.text.includes(".npmrc")||p.text.includes(".ssh"));e.push(a("SUPPLY-NPMRC-NET-001","Install-time code reads npm/ssh credentials and uses the network","static: credential file read combined with http(s)/fetch",c?.file,"critical"));}for(let c of o)/eval\s*\(\s*(Buffer|atob|fromCharCode|decodeURIComponent)/.test(c.text)&&e.push(a("SUPPLY-OBFUSCATE-001","Package evals a decoded/obfuscated string","static: eval of decoded payload",c.file));return v()&&(e.push(..._(t)),e.push(...A(t))),e}function B(t,e){let n=existsSync(join(t,"package.json"))?b(join(t,"package.json")):{},r=existsSync(join(e,"package.json"))?b(join(e,"package.json")):{},i=n.scripts??{},s=r.scripts??{},o=I.filter(u=>typeof s[u]=="string"&&i[u]===void 0),m=f(t).map(u=>readFileSync(u,"utf8")).join(`
|
|
2
2
|
`),h=f(e).map(u=>readFileSync(u,"utf8")).join(`
|
|
3
|
-
`),c=/child_process/.test(h)&&!/child_process/.test(m),
|
|
3
|
+
`),c=/child_process/.test(h)&&!/child_process/.test(m),p=/https?\.request|fetch\s*\(/.test(h)&&!/https?\.request|fetch\s*\(/.test(m);return o.length===0&&!c&&!p?[]:[a("SUPPLY-BEHAVIOR-001","Package version adds install-script or process/network behavior",`added scripts=${o.join(",")||"none"} child_process=${c} network=${p}`,"package.json")]}function V(t,e,n=72,r=Date.now()){let i=t.time?.[e];if(!i)return [];let s=Date.parse(i);if(Number.isNaN(s))return [];let o=(r-s)/36e5;return o>=n?[]:[a("SUPPLY-COOLDOWN-001","Package version is younger than the release cooldown",`${e} published ${o.toFixed(1)}h ago (min ${n}h)`,void 0,"medium")]}function z(t){let e=[];for(let[n,r]of Object.entries(t.packages??{})){if(n==="")continue;let i=r.resolved;if(i){(r.integrity===void 0||r.integrity.length===0)&&e.push(a("SUPPLY-LOCKINTEGRITY-001","Lockfile entry has no integrity hash",`${n} resolved without an integrity hash`,"package-lock.json"));try{let s=new URL(i);(s.protocol!=="https:"||s.hostname!==y)&&e.push(a("SUPPLY-LOCKURL-001","Lockfile entry resolves from a non-npm registry host",`${n} resolved host ${s.hostname||i}`,"package-lock.json"));}catch{e.push(a("SUPPLY-LOCKURL-001","Lockfile entry has a non-URL resolved field",`${n} resolved ${i}`,"package-lock.json"));}}}return e}function q(t,e,n){let r=t.startsWith("@"),i=/@[\w-]+\s*:\s*registry\s*=/.test(e)||/registry\s*=\s*https?:\/\/(?!registry\.npmjs\.org)/.test(e);return r&&i&&n?[a("SUPPLY-CONFUSION-001","Private-scope name also exists on the public registry",`${t} is configured for a private registry but a public package exists`)]:[]}function J(t,e){return t&&!e?[a("SUPPLY-PROVENANCE-001","Package dropped npm provenance since the last scan","previous version had provenance; current version does not")]:[]}async function X(t,e={}){if(!e.experimental&&process.env.FULLSHIELD_EXPERIMENTAL_SUPPLY!=="1")return {findings:[],skipped:true};let n=C(t),r=`https://${y}/${encodeURIComponent(n)}`;j(r),e.fetchPackument&&await e.fetchPackument(r);let i=e.extractDir!==void 0?$(e.extractDir):[];return i.length===0&&e.runNpm&&await e.runNpm(["install",t,"--ignore-scripts"]),{findings:i}}var x=/(?:~\/)?\.npmrc\b|\bNPM_TOKEN\b|\bGITHUB_TOKEN\b/,P=/\bnpm\s+publish\b|\bnpmPublish\b|\blibnpmpublish\b/,R=/registry\.npmjs\.org[\s\S]{0,800}\bPUT\b|\bmethod\s*:\s*['"]PUT['"][\s\S]{0,400}registry\.npmjs\.org/,w=/api\.github\.com\/(?:user\/repos|repos\/)|\/actions\/workflows/,E=/\.github\/workflows/,k=/\.github\/|\.git\/hooks\/|\.gitlab-ci\.yml|\.travis\.yml|azure-pipelines(?:\.yml)?|Jenkinsfile|\.circleci\//;function U(t){let e=join(t,"package.json"),n="";if(existsSync(e)&&statSync(e).isFile())try{let s=b(e).scripts??{};n=Object.values(s).join(`
|
|
4
4
|
`);}catch{n="";}return {sources:f(t).map(i=>({file:relative(t,i)||i,text:readFileSync(i,"utf8")})),scripts:n}}function _(t){let{sources:e,scripts:n}=U(t);if(e.length===0&&n.length===0)return [];let r=`${e.map(o=>o.text).join(`
|
|
5
5
|
`)}
|
|
6
|
-
${n}`;if(!
|
|
6
|
+
${n}`;if(!x.test(r))return [];if(!(P.test(r)||R.test(r)||w.test(r)||E.test(r)))return [];let s=e.find(o=>x.test(o.text)&&(P.test(o.text)||R.test(o.text)||w.test(o.text)||E.test(o.text)))??e.find(o=>x.test(o.text));return [a("SUPPLY-WORM-001","Self-replicating worm indicators in dependency code","static: npm/GitHub token read combined with publish, registry PUT, GitHub repo/workflow creation, or writing .github/workflows",s?.file,"critical")]}function A(t){let{sources:e,scripts:n}=U(t),r=`${e.map(s=>s.text).join(`
|
|
7
7
|
`)}
|
|
8
8
|
${n}`;if(!k.test(r))return [];let i=e.find(s=>k.test(s.text));return [a("SUPPLY-WORKFLOW-001","Dependency writes GitHub workflow, git hooks, or CI config","static: install script or package code writes .github/, .git/hooks/, or CI configuration",i?.file??(k.test(n)?"package.json":void 0),"critical")]}function Q(t,e){let n=0,r=0;for(let s of t){let o=e.get(`${s.name}@${s.version}`);o===true?n+=1:o===void 0&&(r+=1);}let i=t.length;return {total:i,withProvenance:n,unknown:r,percent:i===0?0:n/i*100}}export{$ as analyzePackageDir,A as analyzeWorkflowTampering,_ as analyzeWormIndicators,j as assertRegistryUrl,V as checkCooldown,q as checkDependencyConfusion,z as checkLockfileIntegrity,J as checkProvenance,B as diffPackageVersions,X as installGuard,C as packageNameFromSpec,Q as provenanceCoverage};
|
package/models/index.json
CHANGED
package/package.json
CHANGED
|
@@ -9,7 +9,7 @@ inputs:
|
|
|
9
9
|
version:
|
|
10
10
|
description: 'Pinned fullshield npm version'
|
|
11
11
|
required: false
|
|
12
|
-
default: '
|
|
12
|
+
default: '3.0.0'
|
|
13
13
|
fail-on:
|
|
14
14
|
description: 'Minimum severity that fails the job'
|
|
15
15
|
required: false
|
|
@@ -45,7 +45,7 @@ runs:
|
|
|
45
45
|
uses: actions/setup-node@v7
|
|
46
46
|
with:
|
|
47
47
|
node-version: '22'
|
|
48
|
-
- name: Install fullshield@
|
|
48
|
+
- name: Install fullshield@3.1.1
|
|
49
49
|
shell: bash
|
|
50
50
|
run: npm install --global fullshield@${{ inputs.version }}
|
|
51
51
|
- name: Cache FullShield baseline
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
name: FullShield bumps
|
|
2
|
+
on:
|
|
3
|
+
schedule:
|
|
4
|
+
- cron: '0 7 * * *'
|
|
5
|
+
permissions:
|
|
6
|
+
contents: read
|
|
7
|
+
jobs:
|
|
8
|
+
plan:
|
|
9
|
+
runs-on: ubuntu-24.04
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v7
|
|
12
|
+
- run: npx fullshield@3.1.1 bumps plan --from .fullshield/last-scan.json
|
|
13
|
+
env:
|
|
14
|
+
FULLSHIELD_EXPERIMENTAL_BUMP_PR: '1'
|
|
15
|
+
- run: npx fullshield@3.1.1 bumps open --from .fullshield/last-scan.json
|
|
16
|
+
env:
|
|
17
|
+
FULLSHIELD_EXPERIMENTAL_BUMP_PR: '1'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
name: FullShield diff
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
permissions:
|
|
5
|
+
contents: read
|
|
6
|
+
jobs:
|
|
7
|
+
diff:
|
|
8
|
+
runs-on: ubuntu-24.04
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v7
|
|
11
|
+
with:
|
|
12
|
+
fetch-depth: 0
|
|
13
|
+
- run: npx fullshield@3.1.1 scan --diff
|
|
14
|
+
env:
|
|
15
|
+
FULLSHIELD_EXPERIMENTAL_DIFFSCAN: '1'
|