fullshield 3.0.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/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 x(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=x(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(d=>d.text.includes(".npmrc")||d.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"))?x(join(t,"package.json")):{},r=existsSync(join(e,"package.json"))?x(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(`
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),d=/https?\.request|fetch\s*\(/.test(h)&&!/https?\.request|fetch\s*\(/.test(m);return o.length===0&&!c&&!d?[]:[a("SUPPLY-BEHAVIOR-001","Package version adds install-script or process/network behavior",`added scripts=${o.join(",")||"none"} child_process=${c} network=${d}`,"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 b=/(?:~\/)?\.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=x(e).scripts??{};n=Object.values(s).join(`
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(!b.test(r))return [];if(!(P.test(r)||R.test(r)||w.test(r)||E.test(r)))return [];let s=e.find(o=>b.test(o.text)&&(P.test(o.text)||R.test(o.text)||w.test(o.text)||E.test(o.text)))??e.find(o=>b.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(`
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
@@ -1,5 +1,5 @@
1
1
  {
2
- "fullshieldVersion": "3.0.0",
2
+ "fullshieldVersion": "3.1.1",
3
3
  "entries": [
4
4
  {
5
5
  "package": "express",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullshield",
3
- "version": "3.0.0",
3
+ "version": "3.1.1",
4
4
  "description": "Full-cycle security scanner for Node.js/TypeScript backends: detect, classify, report, notify, fix, prevent.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -45,7 +45,7 @@ runs:
45
45
  uses: actions/setup-node@v7
46
46
  with:
47
47
  node-version: '22'
48
- - name: Install fullshield@3.0.0
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'
@@ -15,7 +15,7 @@ jobs:
15
15
  # Example Marketplace ref once published: kathanp/fullshield-action@<sha>
16
16
  - uses: ./templates/action
17
17
  with:
18
- version: '3.0.0'
18
+ version: '3.1.1'
19
19
  fail-on: high
20
20
  cache-baseline: 'true'
21
21
  upload-sarif: 'true'
@@ -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'