${esc(s.detail)}
${link(s.url,'Source')} · source/checked time: ${esc(s.date)}'use strict'; const $=id=>document.getElementById(id), esc=s=>String(s??'').replace(/[&<>"']/g,c=>({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); const link=(url,label)=>`${esc(label)}`; let report=null, previous=null, previousRepo=null, timer=null; async function json(url,options={}){const response=await fetch(url,{...options,signal:AbortSignal.timeout(15000)});if(!response.ok)throw new Error(`HTTP ${response.status}${response.status===403||response.status===429?' (API limit or denied access)':''}`);return response.json();} function parseRepo(value){let s=value.trim().replace(/\/$/,'');if(s.startsWith('https://github.com/'))s=s.slice(19);if(!/^[\w.-]+\/[\w.-]+$/.test(s))throw new Error('Enter owner/repository or an https://github.com/owner/repository URL. Public GitHub only in v0.');return s.replace(/\.git$/,'');} function parseFile(path,text,url,items,gaps){const add=(name,version,ecosystem,exact=false,coverage='Declared; installed version unknown')=>items.push({name,version,ecosystem,exact,path,url,coverage}); try{ if(path.endsWith('package.json')){const p=JSON.parse(text);for(const kind of ['dependencies','devDependencies','peerDependencies','optionalDependencies'])for(const [n,v]of Object.entries(p[kind]||{}))add(n,v,'npm',false,kind);for(const[n,v]of Object.entries(p.engines||{}))add(n,v,'runtime');} else if(path.endsWith('package-lock.json')){const p=JSON.parse(text);if(p.packages){for(const[n,v]of Object.entries(p.packages)){if(n.startsWith('node_modules/')&&!n.slice(13).includes('/node_modules/')&&v.version)add(n.slice(13),v.version,'npm',true,'Resolved lockfile version; runtime not verified');}}else for(const[n,v]of Object.entries(p.dependencies||{}))if(v.version)add(n,v.version,'npm',true,'Resolved lockfile version');} else if(/requirements[^/]*\.txt$/.test(path)){for(const line of text.split('\n')){const m=line.match(/^([\w.-]+)(?:\[[^\]]+\])?\s*(==|>=|~=|<=|>|<)?\s*([^;#\s]*)/);if(m)add(m[1],m[3]||'unversioned','PyPI',m[2]==='==',m[2]==='=='?'Exact manifest pin':'Range / installed version unknown');}} else if(/Dockerfile[^/]*$/.test(path)){for(const m of text.matchAll(/^\s*FROM\s+(?:--platform=\S+\s+)?(\S+)/gmi))add(m[1].split(':')[0],m[1],'container');} else if(path.includes('.github/workflows/')){for(const m of text.matchAll(/\buses:\s*['"]?([^\s'"#]+)/g))add(m[1].split('@')[0],m[1].split('@')[1]||'unversioned','action');} else {add(path.split('/').pop(),'Detected','stack file');gaps.push(`${path}: detected but full parser unavailable (including yarn/pnpm/Python project lock formats).`);} }catch(e){gaps.push(`${path}: parse failed: ${e.message}`);}} function signal(title,detail,url,date,severity='info'){report.signals.push({title,detail,url,date,severity});} async function scan(){if($('run').disabled)return;let repo;try{repo=parseRepo($('repo').value)}catch(e){$('status').textContent=e.message;return;}$('run').disabled=true;$('demo').disabled=true;$('export').disabled=true;$('results').hidden=true;$('onboarding').hidden=true;$('public-consent').checked=false; report={repo,checkedAt:new Date().toISOString(),items:[],signals:[],gaps:[],files:[]};const gaps=report.gaps;try{ $('status').textContent='Reading repository metadata…';const api=`https://api.github.com/repos/${repo}`;const meta=await json(api);const branch=meta.default_branch;const commit=await json(`${api}/commits/${encodeURIComponent(branch)}`);report.commit=commit.sha;report.branch=branch; const tree=await json(`${api}/git/trees/${commit.sha}?recursive=1`);if(tree.truncated)gaps.push('GitHub tree is truncated; inventory may omit files.');const paths=tree.tree.filter(f=>f.type==='blob'&&/(^|\/)(package\.json|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|requirements[^/]*\.txt|pyproject\.toml|poetry\.lock|uv\.lock|go\.mod|Cargo\.toml|Dockerfile[^/]*|\.nvmrc|\.node-version)$|(^|\/)\.github\/workflows\/[^/]+\.ya?ml$/.test(f.path));if(paths.length>24)gaps.push(`${paths.length} relevant files found; first 24 scanned.`); for(const f of paths.slice(0,24)){$('status').textContent=`Inspecting ${f.path}…`;const url=`https://github.com/${repo}/blob/${commit.sha}/${f.path}`;try{if(f.size>400000){gaps.push(`${f.path}: exceeds 400 KB limit.`);continue;}const blob=await json(`${api}/git/blobs/${f.sha}`);const bytes=Uint8Array.from(atob(blob.content.replace(/\s/g,'')),c=>c.charCodeAt(0));const text=new TextDecoder().decode(bytes);report.files.push({path:f.path,url});parseFile(f.path,text,url,report.items,gaps);}catch(e){gaps.push(`${f.path}: ${e.message}`);}} if(!report.items.length)gaps.push('No supported stack declarations found. This is not evidence of a dependency-free repository.'); const unique=new Map();for(const i of report.items){const key=i.ecosystem+':'+i.name;const old=unique.get(key);if(!old||i.exact&&!old.exact)unique.set(key,i);}const dependencies=[...unique.values()].filter(i=>['npm','PyPI'].includes(i.ecosystem));if(dependencies.length>12)gaps.push(`${dependencies.length} dependencies inventoried; release/advisory checks limited to first 12 unique dependencies.`); for(const i of dependencies.slice(0,12)){$('status').textContent=`Checking upstream signals for ${i.name}…`;if(i.ecosystem==='npm')try{const p=await json(`https://registry.npmjs.org/${encodeURIComponent(i.name)}`);const latest=p['dist-tags']?.latest, current=i.exact?p.versions?.[i.version]:null;signal(`${i.name} · latest ${latest||'unknown'}`,`Observed ${i.version}. ${i.exact&&latest===i.version?'Matches npm latest.': 'Review compatibility and release notes; a differing version/range is not proof an upgrade is required.'}`,`https://www.npmjs.com/package/${encodeURIComponent(i.name)}`,p.time?.[latest]||report.checkedAt);if(current?.deprecated)signal(`${i.name} · deprecated version`,current.deprecated,`https://www.npmjs.com/package/${encodeURIComponent(i.name)}/v/${encodeURIComponent(i.version)}`,report.checkedAt,'alert');else if(!i.exact)gaps.push(`${i.name}: installed version and version-specific deprecation unknown (range).`);}catch(e){gaps.push(`${i.name}: npm release/deprecation check unavailable: ${e.message}`);} if(i.exact&&/^\d+\.\d+/.test(i.version))try{const result=await json('https://api.osv.dev/v1/query',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({package:{name:i.name,ecosystem:i.ecosystem},version:i.version})});const active=(result.vulns||[]).filter(v=>!v.withdrawn);if(!active.length)signal(`${i.name} ${i.version} · no OSV matches`,`OSV returned no published matches for this exact version at scan time. This is not a security certification.`, 'https://osv.dev/',report.checkedAt);for(const v of active)signal(`${i.name} ${i.version} · ${v.id}`,`${v.summary||'Published vulnerability'} — lockfile/pin matches; runtime reachability and exploitability require review.`,`https://osv.dev/vulnerability/${encodeURIComponent(v.id)}`,v.modified||v.published||report.checkedAt,'alert');}catch(e){gaps.push(`${i.name}: OSV check unavailable: ${e.message}`);}else gaps.push(`${i.name}: vulnerability status unknown; no supported exact version.`);} try{const releases=await json(`${api}/releases?per_page=3`);for(const r of releases)signal(`Repository release · ${r.tag_name}`,`${r.name||r.tag_name}${r.prerelease?' (prerelease)':''}. Review release notes for changes affecting this stack.`,r.html_url,r.published_at);if(!releases.length)gaps.push('Repository has no GitHub releases; tags/changelog not evaluated.');}catch(e){gaps.push(`Repository releases unavailable: ${e.message}`);} gaps.push('Docker and Action refs: inventory only; release, security and deprecation coverage unknown. Private repos, GitLab and runtime discovery are unsupported in this tab. Hosted pilot coverage is shown separately.'); const signature=JSON.stringify(report.items.map(i=>[i.name,i.version,i.path]));report.comparison=previous&&previousRepo===repo?(signature===previous?'No inventory changes since previous scan in this tab.':'Inventory changed since previous scan in this tab; review evidence.'):'First scan in this tab; no previous baseline.';previous=signature;previousRepo=repo; $('status').textContent=`Scan complete · ${report.checkedAt} · ${repo}`; }catch(e){gaps.push(`Scan incomplete: ${e.message}. Check repository visibility, connectivity and GitHub's unauthenticated rate limit (usually 60 requests/hour/IP). Retry later; no clean bill of health is implied.`);$('status').textContent='Scan incomplete — see unknowns below.';} render();$('run').disabled=false;$('demo').disabled=false;$('export').disabled=false;} function render(){$('results').hidden=false;$('onboarding').hidden=!report.files.length;$('summary').innerHTML=`
${report.files.length?'Live API evidence':'No repository evidence collected'} ${report.items.length} observations · ${report.files.length} files · ${esc(report.checkedAt)}
${esc(report.comparison||'Incomplete scan; no baseline saved.')} ${report.commit?link(`https://github.com/${report.repo}/tree/${report.commit}`,`Source ${report.commit.slice(0,8)}`):''}
`;$('inventory').innerHTML=report.items.map(i=>`${esc(s.detail)}
${link(s.url,'Source')} · source/checked time: ${esc(s.date)}No verified signals collected. See coverage gaps.
';$('gaps').innerHTML=report.gaps.map(g=>`