
    `gj{N                    N   d Z ddlmZ ddlZddlZddlZddlZddlZddl	Z
ddlZ
ddlmZmZ ddlmZ ddlmZmZ ddlmZ dZd	Zd
ZdZdZdddddddZ ed       G d d             Ze G d d             Ze G d d             Zd0dZ ej@                  dejB                        Z"d1dZ#d1dZ$d2dZ% ej@                  d      Z& ej@                  d       Z'd3d!Z(d0d"Z)d4d#Z*d5d$Z+d6d%Z,d7d&Z-d8d'Z.d9d(Z/d)d)d)dd*	 	 	 	 	 	 	 	 	 d:d+Z0d;d,Z1d;d-Z2	 	 	 	 	 	 	 	 	 	 d<d.Z3d=d/Z4y)>uS  On-demand supply-chain audit for Hermes Agent installs.

Scans three surfaces a Hermes user actually controls and we can map to
upstream advisories without auth or extra binaries:

1. The Hermes venv (every PyPI dist via ``importlib.metadata``).
2. Python deps declared by user-installed plugins under ``~/.hermes/plugins``
   (``requirements.txt`` + ``pyproject.toml`` best-effort pin extraction).
3. MCP servers wired in ``config.yaml`` whose ``command/args`` look like
   ``npx -y <pkg>@<ver>`` or ``uvx <pkg>==<ver>``.

Vulnerabilities are looked up against OSV.dev (``api.osv.dev/v1/querybatch``
+ ``/v1/vulns/{id}``). Single-shot, on-demand, never daily — see the design
notes in ``references/security-disclosure-triage.md``.

Out of scope on purpose: global pip/npm, editor/browser extensions,
daily background scans, auto-blocking installs.
    )annotationsN)	dataclassfield)Path)IterableOptional)get_hermes_homez!https://api.osv.dev/v1/querybatchz"https://api.osv.dev/v1/vulns/{vid}i                    )UNKNOWNLOWMODERATEMEDIUMHIGHCRITICALT)frozenc                  :    e Zd ZU dZded<   ded<   ded<   ded<   y)	Componentz=A single (name, version, ecosystem) tuple discovered on disk.strnameversion	ecosystemsourceN)__name__
__module____qualname____doc____annotations__     L/root/.hermes/venv/lib/python3.12/site-packages/hermes_cli/security_audit.pyr   r   9   s    G
ILNKr$   r   c                  P    e Zd ZU ded<   dZded<   dZded<    ee      Zded	<   y
)Vulnerabilityr   osv_idr   severity summary)default_factory	list[str]fixed_versionsN)	r   r   r    r"   r)   r+   r   listr.   r#   r$   r%   r'   r'   C   s*    KHcGS %d ;NI;r$   r'   c                  "    e Zd ZU ded<   ded<   y)Findingr   	componentr'   vulnN)r   r   r    r"   r#   r$   r%   r1   r1   K   s    
r$   r1   c            	     v   ddl m}  g }t               } |        D ]  }	 |j                  d   xs dj	                         }|j                  xs dj	                         }|r|sH|j                         |f}||v r_|j                  |       |j                  t        ||dd              |S # t
        $ r Y w xY w)z9Every dist installed in the running Python's import path.r   )distributionsNamer*   PyPIvenvr   r   r   r   )importlib.metadatar5   setmetadatastrip	Exceptionr   loweraddappendr   )r5   outseendistr   r   keys          r%   _discover_venvrF   T   s    0C!$D [	MM&)/R668D <<%2,,.7zz|W%$;

9$6RXYZ[ J  		s   !B,,	B87B8z^\s*
        (?P<name>[A-Za-z0-9][A-Za-z0-9._-]*)
        (?:\[[^\]]+\])?              # extras
        \s*==\s*
        (?P<version>[A-Za-z0-9._+!-]+)
        \s*(?:;.*)?$
    c                0   g }| j                         D ]  }|j                         }|r"|j                  d      s|j                  d      r8t        j	                  |      }|sP|j                  |j                  d      |j                  d      f        |S )u
  Extract ``name==version`` pins. Everything else (>=, ~=, no pin) is skipped.

    A loose pin can't be mapped to a single OSV query, and getting it wrong
    is worse than missing a finding for an audit tool — false positives
    train users to ignore output.
    #-r   r   )
splitlinesr=   
startswith	_REQ_LINEmatchrA   group)textpinsrawlinems        r%   _parse_requirementsrT   w   s     #%D  ?yy{ts+ts/COOD!KK!'')*<=>? Kr$   c                   	 ddl }	 |j                  |       }g }|j	                  d      xs i }t        |j	                  d      t              r|j                  d |d   D               |j	                  d      xs i }t        |t              r>|j                         D ]+  }t        |t              s|j                  d |D               - g }|D ]K  }t        j                  |      }	|	s|j                  |	j                  d      |	j                  d	      f       M |S # t        $ r g cY S w xY w# t        $ r g cY S w xY w)
zPull ``name==version`` pins from a ``pyproject.toml`` ``dependencies`` list.

    Uses stdlib ``tomllib`` (3.11+). Same exact-pin policy as requirements.
    r   Nprojectdependenciesc              3  2   K   | ]  }t        |        y wNr   .0xs     r%   	<genexpr>z(_parse_pyproject_pins.<locals>.<genexpr>   s     <qCF<   zoptional-dependenciesc              3  2   K   | ]  }t        |        y wrY   rZ   r[   s     r%   r^   z(_parse_pyproject_pins.<locals>.<genexpr>   s     2qCF2r_   r   r   )tomllibImportErrorloadsr>   get
isinstancer/   extenddictvaluesrL   rM   rA   rN   )
rO   ra   datadepsrV   optionalrN   rP   deprS   s
             r%   _parse_pyproject_pinsrm      s,   
}}T" Dhhy!'RG'++n-t4<GN$;<<{{239rH(D!__& 	3E%&2E22	3 #%D ?OOC KK!'')*<=>? K)  	  	s"   D  D1  D.-D.1D?>D?c                   | dz  }|j                         sg S g }t        |j                               D ]  }|j                         r|j                  j	                  d      r/d|j                   }dD ]]  }||z  }|j                         s	 t        |j                  dd            }|D ]#  \  }}	|j                  t        ||	d|	             % _ |d
z  }
|
j                         s	 t        |
j                  dd            }|D ]#  \  }}	|j                  t        ||	d|	             %  |S # t        $ r Y w xY w# t        $ r Y w xY w)a  Python deps declared by plugins under ``~/.hermes/plugins``.

    Plugins typically don't install into the venv (they're directory-based
    with relative imports), so their stated requirements are useful audit
    surface even when the venv scan misses them.
    plugins.zplugin:)zrequirements.txtzrequirements-dev.txtutf-8replace)encodingerrorsr7   r9   zpyproject.toml)is_dirsortediterdirr   rK   is_filerT   	read_textOSErrorrA   r   rm   )hermes_homeplugins_dirrB   
plugin_dirr   req_filepathrP   r   r   	pyprojects              r%   _discover_pluginsr      sz    	)K	C[0023 c
  "joo&@&@&E:??+,D 	gH(D||~.t~~wW`~/abD &* gMD'JJydGv^defg	g !11	,Y-@-@'Zc-@-de "& cg

9$6Z`abc'c* J    s$   D2*E2	D>=D>	EEzG^(@[A-Za-z0-9._-]+/[A-Za-z0-9._-]+|[A-Za-z0-9._-]+)@([A-Za-z0-9._+-]+)$z3^([A-Za-z0-9][A-Za-z0-9._-]*)==([A-Za-z0-9._+!-]+)$c                2   |xs dj                         j                         }|sy|j                  d      s|dk(  rc|D ]^  }|j                  d      rt        j                  |      }|r2t        |j                  d      |j                  d      dd|  	      c S  y |j                  d
      s|d
k(  rc|D ]^  }|j                  d      rt        j                  |      }|r2t        |j                  d      |j                  d      dd|  	      c S  y y)zBest-effort: parse `command/args` into a (name, version, ecosystem).

    Returns None when the entry doesn't pin a version we can audit (local
    paths, Docker images, unversioned npx, etc.). Audit output stays silent
    rather than guess.
    r*   NnpxrI   r   r   npmzmcp:r9   uvxr7   )	r=   r?   endswithrK   _NPX_PKGrM   r   rN   _UVX_PKG)server_namecommandargscmdtokenrS   s         r%   _extract_mcp_componentr      s    =b


!
'
'
)C
||EcUl 	E$u%A GGAJ#!+/	  	 ||EcUl 	E$u%A GGAJ$!+/	  	 r$   c                    	 ddl m}  g } |        }t        |t              sg S |j                         D ]  \  }}t        |t              s|j                  dd      xs d}|j                  d      xs g }t        |t              sSt        |||D cg c]  }t        |       c}      }|z|j                  |        |S # t        $ r g cY S w xY wc c}w )z0Pinned MCP server packages from ``config.yaml``.r   )_get_mcp_serversr   r*   r   )hermes_cli.mcp_configr   r>   re   rg   itemsrd   r/   r   r   rA   )	r   rB   serversr   cfgr   r   acomps	            r%   _discover_mcpr     s    : C Ggt$	]]_ 		c#t$'')R(.Bwwv$"$%%dGd5Kc!f5KLJJt	 J#  	 6Ls   C CCCc                n   t        j                  |      j                  d      }t        j                  j                  | |ddid      }t        j                  j                  |t              5 }t        j                  |j                         j                  d            cd d d        S # 1 sw Y   y xY w)Nrq   zContent-Typezapplication/jsonPOST)ri   headersmethodtimeout)jsondumpsencodeurllibrequestRequesturlopenHTTP_TIMEOUTrc   readdecode)urlpayloadri   reqresps        r%   _http_post_jsonr     s    ::g%%g.D
..
 
 $1C DV ! C 
		\		: 7dzz$))+,,W567 7 7s   /2B++B4c                   t         j                  j                  | d      }t         j                  j                  |t              5 }t        j                  |j                         j                  d            cd d d        S # 1 sw Y   y xY w)NGET)r   r   rq   )	r   r   r   r   r   r   rc   r   r   )r   r   r   s      r%   _http_get_jsonr   &  sf    
..
 
 U
 
3C			\		: 7dzz$))+,,W567 7 7s   2BBc                |   | si S i }t        dt        |       t              D ]  }| ||t        z    }d|D cg c](  }|j                  |j                  d|j
                  d* c}i}	 t        t        |      }|j                  d      xs g }t        ||      D ]V  \  }	}
|
xs i j                  d      xs g }|D cg c]%  }|j                  d	      s|j                  d	      ' }}|sR|||	<   X  |S c c}w # t        j                  j                  t        t        f$ r}t        d|       |d}~ww xY wc c}w )
zReturn {component -> [osv_id, ...]} for components with any vulns.

    Components without findings are omitted from the result dict.
    r   queries)r   r   )packager   zOSV batch query failed: Nresultsvulnsid)rangelenOSV_BATCH_MAXr   r   r   r   OSV_BATCH_URLr   errorURLErrorTimeoutErrorConnectionErrorRuntimeErrorrd   zip)
componentsfindingschunk_startchunkcr   r   excr   r   resultr   vidss                 r%   _osv_query_batchr   ,  sP   
 	+-HQJ? %;{]'BC
 
  )*Q[[I yy
	J"=':D ((9%+w/ 	%LD&\r&&w/52E(-=1t155;=C=!$		%!%* O% %%|_E 	J!9#?@cI	J
 >s)   -C5'C:;D9D9:(D6"D11D6c                   | j                  d      xs i }|j                  d      }t        |t              r8|j                         r(|j                         j	                         }|t
        v r|S d}| j                  d      xs g D ]$  }|j                  d      }t        |t              s%& | j                  d      xs g }|D ]}  }|j                  d      xs i }	|	j                  d      }
t        |
t              s:|
j                         j	                         t
        v s_|
j                         j	                         c S  ||dk\  ry|d	k\  ry
|dk\  ry|dkD  ryy)z;Extract CVSS-derived severity tier from an OSV vuln record.database_specificr)   Nscoreaffectedecosystem_specificg      "@r   g      @r   g      @r   r   r   r   )rd   re   r   r=   upperSEVERITY_ORDER)recorddb_specificrQ   r   r   	sev_entrysr   entryeco_specsevs              r%   _osv_severity_from_recordr   L  sF    **017RK
//*
%C#s				!!#N"L!EZZ
+1r 	MM'"a  zz*%+H '99128bll:&c3CIIK$5$5$7>$I99;$$&&	'
 C<C<C<19r$   c           	     ^   g }| j                  d      xs g D ]X  }|j                  d      xs g D ]>  }|j                  d      xs g D ]$  }d|v s|j                  t        |d                & @ Z t               }g }|D ])  }||vs|j	                  |       |j                  |       + |S )Nr   rangeseventsfixed)rd   rA   r   r;   r@   )r   fixesr   rngeventrC   rB   fs           r%   _osv_fixed_versionsr   q  s    EJ'-2 699X&," 	6C*0b 6e#LLU7^!456	66 UDC D=HHQKJJqM Jr$   c                   t        | D ch c]  }|s|	 c}      }|si S i }dd}t        j                  j                  t              5 }|j                  ||      D ]  }|||j                  <    	 ddd       |S c c}w # 1 sw Y   |S xY w)z<Fetch summary/severity for each unique vuln id, in parallel.c                :   	 t        t        j                  |             }t        | t        |      |j                  d      xs dj                         t        |            S # t        j                  j
                  t        t        f$ r t        |       cY S w xY w)N)vidr(   r+   r*   )r(   r)   r+   r.   )r   OSV_VULN_URLformatr   r   r   r   r   r'   r   rd   r=   r   )r   recs     r%   
_fetch_onez&_osv_fetch_details.<locals>._fetch_one  s    	- !4!4!4!=>C .s3WWY'-2446.s3	
 	
 %%|_E 	- ,,	-s   A# #4BB)max_workersN)r   r   returnr'   )rv   
concurrentfuturesThreadPoolExecutorDETAIL_PARALLELISMmapr(   )vuln_idsr   uniquerB   r   poolr3   s          r%   _osv_fetch_detailsr     s    H4SS45F	$&C

 
			.	.;M	.	N $RVHHZ0 	$D#C	$$ J) 5"$ Js   A=A='BBF	skip_venvskip_pluginsskip_mcpr{   c                F   |xs t        t                     }g }| s|j                  t                      |s|j                  t	        |             |s|j                  t                      |sg S t        |      }|sg S g }|j                         D ]  }|j                  |        t        |      }	g }
|j                         D ]G  \  }}|D ]=  }|	j                  |      xs t        |      }|
j                  t        ||             ? I |
j                  d        |
S )zHDiscover components, query OSV, return findings sorted by severity desc.r   )r2   r3   c                    t         j                  | j                  j                  d       | j                  j
                  | j                  j                  j                         | j                  j                  fS Nr   )	r   rd   r3   r)   r2   r   r   r?   r(   )r   s    r%   <lambda>zrun_audit.<locals>.<lambda>  sR    33KKKK""$FFMM	
 r$   )rE   )r   r	   rf   rF   r   r   r   rh   r   r   rd   r'   rA   r1   sort)r   r   r   r{   homer   rQ   all_idsr   detailsr   r   r   r3   s                 r%   	run_auditr    s    1$01D"$J.*++D12-/*	
:
&C	Gzz| s )G HYY[ @	c 	@C;;s#@}C'@DOOGd>?	@@
 MM
   Or$   c           	        | sd| dS g }|j                  dt        |        d| d       |j                  d       d }| D ]  }|j                  j                  |k7  r?|j                  d|j                  j                   d       |j                  j                  }|j                  j
                  j                  d	      }d
| d
|j                  j                   d|j                  j                   d
|j                  j                   }|j                  |       |j                  j                  r@|j                  j                  }t        |      dkD  r|d d dz   }|j                  d|        |j                  j                  sJ|j                  ddj                  |j                  j                  d d                dj                  |      S )Nz&No known vulnerabilities found across z component(s).zFound z' known vulnerability finding(s) across z component(s):r*   []r   z  z==d   a   z...z           z           fixed in: z, r   
)rA   r   r2   r   r3   r)   ljustr   r   r(   r+   r.   join)r   total_componentslineslast_sourcer   r   headr+   s           r%   _render_humanr    s   78H7IXXE	LL
X  "#>	3 
LLK Y;;,LL1Q[[//023++,,Kffoo##A&C51;;++,Bq{{/B/B.C2affmm_UT66>>ffnnG7|c!!#2,.LL;wi0166  LL0166;P;PQSRS;T1U0VWXY 99Ur$   c                   |t        |       | D cg c]  }|j                  j                  |j                  j                  |j                  j                  |j                  j
                  |j                  j                  |j                  j                  |j                  j                  |j                  j                  d c}d}t        j                  |d      S c c}w )N)r   r   r   r   vuln_idr)   r+   r.   total_components_scannedfinding_countr   r   )indent)r   r2   r   r   r   r   r3   r(   r)   r+   r.   r   r   )r   r
  r   r   s       r%   _render_jsonr    s    $4X 
  ;;++;;..[[22++,,66==FFOO66>>"#&&"7"7	
G" ::ga((
s   B2Cc                    d}| s|t        t                     z  }|s|t        t        |            z  }|s|t        t                     z  }|S r   )r   rF   r   r   )r   r   r   r{   totals        r%   _count_componentsr     sO     E^%&&&{344]_%%Lr$   c                r   t        t                     }t        t        | dd            }t        t        | dd            }t        t        | dd            }t        t        | dd            }t        | dd      xs dj	                         }|t
        vr.t        d	|j                          d
t        j                         yt        ||||      }|dk(  r3d}|r#t        t        j                  ddg d             yt        |       y	 t        ||||      }	|rt        t        |	|             nt        t!        |	|             t
        |   }|	D ]1  }t
        j#                  |j$                  j&                  d      |k\  s1 y y# t        $ r(}
t        d|
 t        j                         Y d}
~
yd}
~
ww xY w)z*Implementation of `hermes security audit`.r   Fr   r   r   fail_onNcriticalzunknown --fail-on value: z- (choose from: low, moderate, high, critical))filer   r   r   zDNo components discovered (everything skipped, or empty environment).r  zaudit failed: r   )r   r	   boolgetattrr   r   printr?   sysstderrr  r   r   r  r   r  r  rd   r3   r)   )r   r   r   r   r   output_jsonr  r  msgr   r   	thresholdr   s                r%   cmd_security_auditr%    s   !"DWT;67Ine<=LGD*e45HwtVU34KtY-;BBDGn$''8 9; <	

 ,W[E zT$**!a]_`ab  #J	%	
 l8U+,mHe,- w'I affooq1Y>   se$3::6s   	F 	F6F11F6)r   list[Component])rO   r   r   zlist[tuple[str, str]])r{   r   r   r&  )r   r   r   r   r   r-   r   zOptional[Component])r   r   r   rg   r   rg   )r   r   r   rg   )r   r&  r   zdict[Component, list[str]])r   rg   r   r   )r   rg   r   r-   )r   zIterable[str]r   zdict[str, Vulnerability])
r   r  r   r  r   r  r{   zOptional[Path]r   list[Finding])r   r'  r
  intr   r   )
r   r  r   r  r   r  r{   r   r   r(  )r   zargparse.Namespacer   r(  )5r!   
__future__r   argparseconcurrent.futuresr   r   rer   urllib.errorr   urllib.requestdataclassesr   r   pathlibr   typingr   r   hermes_constantsr	   r   r   r   r   r   r   r   r'   r1   rF   compileVERBOSErL   rT   rm   r   r   r   r   r   r   r   r   r   r   r   r  r  r  r  r%  r#   r$   r%   <module>r5     s  & #    	 
   (  % ,33   $   < < <   . BJJ JJ		$<!T 2::`a 2::LM'T677@"J"< "&++ + 	+
  + +b8)*

&*
6:
IM

 0r$   