§
    øžyj*?  ã            
      óÊ  — U d Z ddlmZ ddlZddlZddlZddlmZ ddlm	Z	 ddl
mZmZ  ej        e¦  «        Z ed¬¦  «         G d	„ d
¦  «        ¦   «         Z eddddd edh¦  «        ffddd¬¦  «        fZded<    ed¬¦  «         G d„ d¦  «        ¦   «         Zd?d„Zefd@d!„ZdAd#„ZdBd&„ZdCd(„ZdDd)„ZdEd+„ZdFd-„Zd.Zd/ZdGd1„ZdHd3„Z dId6„Z!ed7œdJd:„Z"dKd<„Z#dLd=„Z$dLd>„Z%dS )Ma1  
Security advisory checker for Hermes Agent.

Detects known-compromised Python packages installed in the active venv
(supply-chain attacks like the Mini Shai-Hulud worm of May 2026 that
poisoned ``mistralai 2.4.6`` on PyPI) and surfaces remediation guidance to
the user.

Design goals:

- **Cheap.** A single ``importlib.metadata.version()`` call per advisory
  package. Safe to run on every CLI startup.
- **Loud when it matters, silent otherwise.** If no compromised package is
  installed, the user sees nothing.
- **Acknowledgeable.** Once the user has read and acted on an advisory they
  can dismiss it via ``hermes doctor --ack <id>``; the ack is persisted to
  ``config.security.acked_advisories`` and survives restart.
- **Extensible.** Adding a new advisory is one entry in ``ADVISORIES``;
  adding a new compromised version is a one-line edit. No code changes
  needed when the next worm hits.

The check is invoked from three places:

1. ``hermes doctor`` (and ``hermes doctor --ack <id>``)
2. CLI startup banner (one short line, then full guidance via
   ``hermes doctor``)
3. Gateway startup (logged to gateway.log; first interactive message gets
   a one-line operator banner)

This module is intentionally dependency-free beyond the stdlib so it can
run in environments where the rest of Hermes failed to import.
é    )ÚannotationsN)Ú	dataclass)ÚPath)ÚIterableÚOptionalT)Úfrozenc                  ól   — e Zd ZU dZded<   ded<   ded<   ded<   ded<   d	ed
<   dZded<   dZded<   dS )ÚAdvisoryu(  One security advisory entry.

    Attributes:
        id: stable identifier used for acks (e.g. ``shai-hulud-2026-05``).
            Lowercase-hyphen, never reused.
        title: one-line headline shown in banners.
        summary: 1-3 sentence description of what was compromised and how.
        url: reference URL (Socket advisory, GitHub advisory, PyPI page).
        compromised: tuple of ``(package_name, frozenset_of_versions)``
            pairs. Empty frozenset means "any version of this package is
            considered suspect" â€” use sparingly.
        remediation: ordered list of steps the user should take. First step
            should be the uninstall command; subsequent steps the credential
            audit / rotation guidance.
        published: ISO date string for sort order.
    ÚstrÚidÚtitleÚsummaryÚurlz&tuple[tuple[str, frozenset[str]], ...]Úcompromisedztuple[str, ...]ÚremediationÚ Ú	publishedÚhighÚseverityN)Ú__name__Ú
__module__Ú__qualname__Ú__doc__Ú__annotations__r   r   © ó    úD/home/ragecks/.hermes/hermes-agent/hermes_cli/security_advisories.pyr
   r
   B   s|   € € € € € € ðð ð" €G€G�GØ€J€J�JØ€L€L�LØ€H€H�HØ7Ð7Ð7Ñ7Ø Ð Ð Ñ Ø€IÐÐÐÑØ€HÐÐÐÑÐÐr   r
   zshai-hulud-2026-05u<   Mini Shai-Hulud worm â€” mistralai 2.4.6 compromised on PyPIu`  PyPI quarantined the mistralai package on 2026-05-12 after a malicious 2.4.6 release. The worm steals credentials from environment variables and credential files (~/.npmrc, ~/.pypirc, ~/.aws/credentials, GitHub PATs, cloud SDK tokens) and exfils them to a hardcoded webhook. If you ran any Python process that imported mistralai 2.4.6 â€” including hermes when configured with provider=mistral for TTS or STT â€” assume those credentials are exposed. PyPI has since removed 2.4.6 and the project ships clean releases again (2.4.7, 2.4.8); this advisory only fires if the compromised 2.4.6 is still installed.z1https://socket.dev/blog/mini-shai-hulud-worm-pypiÚ	mistralaiz2.4.6)zARun: pip uninstall -y mistralai  (or: uv pip uninstall mistralai)zlRotate API keys in ~/.hermes/.env (OpenRouter, Anthropic, OpenAI, Nous, GitHub, AWS, Google, Mistral, etc.).z‰Audit ~/.npmrc, ~/.pypirc, ~/.aws/credentials, ~/.config/gh/hosts.yml, and any other credential files for tokens that may have been read.zgCheck GitHub for unexpected new SSH keys, deploy keys, or webhook additions on repos you have admin on.zOAfter cleanup: hermes doctor --ack shai-hulud-2026-05  to dismiss this warning.z
2026-05-12Úcritical)r   r   r   r   r   r   r   r   ztuple[Advisory, ...]Ú
ADVISORIESc                  ó2   — e Zd ZU dZded<   ded<   ded<   dS )ÚAdvisoryHitz.One package-version match against an advisory.r
   Úadvisoryr   ÚpackageÚinstalled_versionN)r   r   r   r   r   r   r   r   r"   r"   ‰   s9   € € € € € € à8Ð8àÐÐÑØ€L€L�LØÐÐÑÐÐr   r"   Úpkg_namer   ÚreturnúOptional[str]c                óº   — 	 ddl m}m} n# t          $ r Y dS w xY w	  || ¦  «        S # |$ r Y dS t          $ r! t
                               d| d¬¦  «         Y dS w xY w)zÔReturn the installed version of ``pkg_name``, or None if not installed.

    Uses ``importlib.metadata`` so we don't depend on pip being importable
    inside the active venv (uv-created venvs may lack pip).
    r   )ÚPackageNotFoundErrorÚversionNz%importlib.metadata.version(%s) raisedT©Úexc_info)Úimportlib.metadatar*   r+   ÚImportErrorÚ	ExceptionÚloggerÚdebug)r&   r*   r+   s      r   Ú_installed_versionr3   ’   s°   € ðØDÐDÐDÐDÐDÐDÐDÐDÐDøÝð ð ð ØˆtˆtðøøøðØˆw�xÑ Ô Ð øØð ð ð ØˆtˆtÝð ð ð õ 	�ŠÐ<¸hÐQUˆÑVÔVÐVØˆtˆtð	øøøs    ‚ ‹
˜�
( ¨A°&AÁAÚ
advisoriesúIterable[Advisory]úlist[AdvisoryHit]c           	     ó¨   — g }| D ]L}|j         D ]B\  }}t          |¦  «        }|€Œ|r||v r%|                     t          |||¬¦  «        ¦  «         ŒCŒM|S )zîScan installed packages and return all advisory hits.

    A "hit" means an advisory's listed package is installed AND the version
    is in the compromised set (or the compromised set is empty, meaning
    *any* version is suspect).
    N)r#   r$   r%   )r   r3   Úappendr"   )r4   Úhitsr#   r&   Úbad_versionsÚ	installeds         r   Údetect_compromisedr<   §   s”   € ð !€DØð 
ð 
ˆØ&.Ô&:ð 		ð 		Ñ"ˆH�lÝ*¨8Ñ4Ô4ˆIØÐ ØØð  9°Ð#<Ð#<Ø—’�KØ%Ø$Ø&/ðñ ô ñ ô ð øð		ð €Kr   úset[str]c                 óT  — 	 ddl m}   | ¦   «         }n:# t          $ r- t                               dd¬¦  «         t          ¦   «         cY S w xY w|                     d¦  «        pi }|                     d¦  «        pg }t          |t          ¦  «        st          ¦   «         S d„ |D ¦   «         S )	uö   Return the set of advisory IDs the user has dismissed.

    Returns an empty set if config can't be loaded (don't block startup
    just because config is broken â€” the advisory will keep firing until
    config is repaired, which is fine).
    r   )Úload_configz'Could not load config for advisory acksTr,   ÚsecurityÚacked_advisoriesc                ó’   — h | ]D}t          |¦  «                             ¦   «         ¯#t          |¦  «                             ¦   «         ’ŒES r   )r   Ústrip)Ú.0Úxs     r   ú	<setcomp>z get_acked_ids.<locals>.<setcomp>Û   s9   € Ð:Ð:Ð:˜q­3¨q©6¬6¯<ª<©>¬>Ð:�C�‰FŒF�LŠL‰NŒNÐ:Ð:Ð:r   )	Úhermes_cli.configr?   r0   r1   r2   ÚsetÚgetÚ
isinstanceÚlist)r?   ÚcfgÚsecÚraws       r   Úget_acked_idsrO   Ê   sÃ   € ðØ1Ð1Ð1Ð1Ð1Ð1Øˆk‰mŒmˆˆøÝð ð ð Ý�ŠÐ>ÈˆÑNÔNÐNÝ‰uŒuˆˆˆðøøøð �'Š'�*Ñ
Ô
Ð
# €CØ
�'Š'Ð$Ñ
%Ô
%Ð
+¨€CÝ�c�4Ñ Ô ð Ý‰uŒuˆØ:Ð: CÐ:Ñ:Ô:Ð:s   ‚ “4A
Á	A
Úadvisory_idÚboolc                óè  — |                       ¦   «         } | sdS 	 ddlm}m} n+# t          $ r t
                               d¦  «         Y dS w xY w	  |¦   «         }|                     di ¦  «        }|                     d¦  «        pg }t          |t          ¦  «        sg }| |vr%|                     | ¦  «         ||d<    ||¦  «         dS # t          $ r t
                               d| ¦  «         Y dS w xY w)	u|   Persist an ack for ``advisory_id``. Returns True on success.

    Idempotent â€” acking an already-acked ID is a no-op.
    Fr   )r?   Úsave_configz-Could not import config module to persist ackr@   rA   Tz%Failed to persist advisory ack for %s)rC   rG   r?   rS   r0   r1   ÚwarningÚ
setdefaultrI   rJ   rK   r8   Ú	exception)rP   r?   rS   rL   rM   Úexistings         r   Úack_advisoryrX   Þ   s>  € ð
 ×#Ò#Ñ%Ô%€KØð ØˆuðØ>Ð>Ð>Ð>Ð>Ð>Ð>Ð>Ð>øÝð ð ð Ý�ŠÐFÑGÔGÐGØˆuˆuðøøøðØˆk‰mŒmˆØ�nŠn˜Z¨Ñ,Ô,ˆØ—7’7Ð-Ñ.Ô.Ð4°"ˆÝ˜(¥DÑ)Ô)ð 	ØˆHØ˜hÐ&Ð&Ø�OŠO˜KÑ(Ô(Ð(Ø&.ˆCÐ"Ñ#ØˆK˜ÑÔÐØˆtøÝð ð ð Ý×ÒÐ@À+ÑNÔNÐNØˆuˆuðøøøs"   š# £$AÁ
AÁA7C Ã%C1Ã0C1r9   c                óD   ‡— | sg S t          ¦   «         Šˆfd„| D ¦   «         S )z=Return only hits whose advisories the user has not dismissed.c                ó0   •— g | ]}|j         j        ‰v¯|‘ŒS r   ©r#   r   )rD   ÚhÚackeds     €r   ú
<listcomp>z"filter_unacked.<locals>.<listcomp>   s'   ø€ Ð:Ð:Ð:�!˜qœzœ}°EÐ9Ð9ˆAÐ9Ð9Ð9r   )rO   )r9   r]   s    @r   Úfilter_unackedr_   û   s3   ø€ àð Øˆ	Ý‰OŒO€EØ:Ð:Ð:Ð:�tÐ:Ñ:Ô:Ð:r   c                 óˆ   — t           j                             d¦  «        rdS t          j                             ¦   «         sdS dS )NÚNO_COLORFT)ÚosÚenvironrI   ÚsysÚstdoutÚisattyr   r   r   Ú_term_supports_colorrg     s=   € Ý	„z‡~‚~�jÑ!Ô!ð ØˆuÝŒ:×ÒÑÔð ØˆuØˆ4r   ú	list[str]c           	     ó&  — | sg S | d         }d|j         j        › d|j         j        › �d|j        › d|j        › �dg}t          | ¦  «        dk    rB|                     ddt          | ¦  «        dz
  › d	t          | ¦  «        d
k    rdnd› d�¦  «         |S )zÉReturn 1-3 short lines suitable for a startup banner.

    Caller is responsible for color/styling. Always names the worst hit
    explicitly so the user knows what's wrong without running doctor.
    r   zSECURITY ADVISORY [z]: z  Detected: ú==z,  Run 'hermes doctor' for remediation steps.é   z  (z additional advisoré   ÚiesÚyz also active.))r#   r   r   r$   r%   ÚlenÚinsert)r9   ÚprimaryÚliness      r   Úshort_banner_linesrs     sÑ   € ð ð Øˆ	Ø�1Œg€GàN˜gÔ.Ô1ÐNÐN°gÔ6FÔ6LÐNÐNØE�w”ÐEÐE¨'Ô*CÐEÐEØ6ð€Eõ
 ˆ4�y„y�1‚}€}Ø�Š�Qð J�c $™iœi¨!™mð Jð JÝ#& t¡9¤9¨q¢= =˜%˜%°cðJð Jð Jñ 	Kô 	Kð 	Kà€Lr   Úhitc                ó  — | j         }d|j        › d�d|j        › d|j        › d|j        › �d| j        › d| j        › �d|j        › �d	|j        d	d
g}t          |j
        d¦  «        D ] \  }}|                     d|› d|› �¦  «         Œ!|S )z@Return a multi-line block describing the advisory + remediation.z=== z ===zID:        z    Severity: z    Published: zDetected:  rj   zReference: r   zRemediation:rk   z  z. )r#   r   r   r   r   r$   r%   r   r   Ú	enumerater   r8   )rt   Úarr   ÚiÚsteps        r   Úfull_remediation_textrz   $  sÀ   € àŒ€AàˆqŒwÐÐÐØR�a”dÐRÐR¨!¬*ÐRÐRÀQÄ[ÐRÐRØ<�c”kÐ<Ð< SÔ%:Ð<Ð<Ø�a”eÐÐØ
Ø	Œ	Ø
Øð	€Eõ ˜Qœ]¨AÑ.Ô.ð 'ð '‰ˆˆ4Ø�ŠÐ%˜!Ð%Ð%˜tÐ%Ð%Ñ&Ô&Ð&Ð&Ø€Lr   Úadvisory_banner_seené   úOptional[Path]c                 ó¨   — 	 ddl m}  t           | ¦   «         ¦  «        dz  }|                     dd¬¦  «         |t          z  S # t
          $ r Y d S w xY w)Nr   )Úget_hermes_homeÚcacheT)ÚparentsÚexist_ok)Úhermes_constantsr   r   ÚmkdirÚ_BANNER_CACHE_FILEr0   )r   Ú	cache_dirs     r   Ú_banner_cache_pathr‡   G  sw   € ðØ4Ð4Ð4Ð4Ð4Ð4Ý˜˜Ñ*Ô*Ñ+Ô+¨gÑ5ˆ	Ø�Š ¨tˆÑ4Ô4Ð4ØÕ-Ñ-Ð-øÝð ð ð Øˆtˆtðøøøs   ‚A A Á
AÁAúdict[str, float]c                 ó¦  — t          ¦   «         } | �|                      ¦   «         si S i }	 |                      d¬¦  «                             ¦   «         D ]k}|                     ¦   «         }|sŒ|                     d d¦  «        }t          |¦  «        dk    rŒC|\  }}	 t          |¦  «        ||<   Œ\# t          $ r Y Œhw xY wn# t          $ r i cY S w xY w|S )Núutf-8©Úencodingrk   rl   )
r‡   ÚexistsÚ	read_textÚ
splitlinesrC   Úsplitro   ÚfloatÚ
ValueErrorr0   )ÚpÚoutÚlineÚpartsrP   Útss         r   Ú_read_banner_cacher˜   Q  s  € ÝÑÔ€AØ€y˜Ÿš™
œ
€yØˆ	Ø€CðØ—K’K¨�KÑ1Ô1×<Ò<Ñ>Ô>ð 	ð 	ˆDØ—:’:‘<”<ˆDØð ØØ—J’J˜t QÑ'Ô'ˆEÝ�5‰zŒz˜QŠˆØØ#‰OˆK˜ðÝ#(¨¡9¤9��KÑ Ð øÝð ð ð Ø�ðøøøð	øõ ð ð ð Øˆ	ˆ	ˆ	ðøøøà€Js6   ªA0B? ÂB.Â-B? Â.
B;Â8B? Â:B;Â;B? Â?CÃCÚseenÚNonec                ó  — t          ¦   «         }|€d S 	 d„ |                      ¦   «         D ¦   «         }|                     d                     |¦  «        dz   d¬¦  «         d S # t          $ r  t
                               dd¬¦  «         Y d S w xY w)Nc                ó"   — g | ]\  }}|› d |› �‘ŒS )Ú r   )rD   Úaidr—   s      r   r^   z'_write_banner_cache.<locals>.<listcomp>m  s&   € Ð;Ð;Ð;¡7 3¨�C��˜"��Ð;Ð;Ð;r   Ú
rŠ   r‹   z%Could not write advisory banner cacheTr,   )r‡   ÚitemsÚ
write_textÚjoinr0   r1   r2   )r™   r“   rr   s      r   Ú_write_banner_cacher£   h  s¤   € ÝÑÔ€AØ€yØˆðMØ;Ð;¨d¯jªj©l¬lÐ;Ñ;Ô;ˆØ	�Š�T—Y’Y˜uÑ%Ô%¨Ñ,°wˆÑ?Ô?Ð?Ð?Ð?øÝð Mð Mð MÝ�ŠÐ<ÀtˆÑLÔLÐLÐLÐLÐLðMøøøs   ”AA! Á!&BÂ
B)Úrepeat_hoursr¤   Úintc               óL  — ddl }t          | ¦  «        }|sg S |                      ¦   «         }t          ¦   «         }||dz  z
  }g }|D ]L}|                     |j        j        d¦  «        }	|	|k     r$|                     |¦  «         |||j        j        <   ŒM|rt          |¦  «         |S )zÍReturn only hits whose banner is due (not acked, not recently shown).

    Side effect: stamps the banner cache for any hit that's about to be
    shown. Callers should subsequently render the result.
    r   Ni  g        )Útimer_   r˜   rI   r#   r   r8   r£   )
r9   r¤   r§   ÚfreshÚnowr€   ÚcutoffÚduert   Úlasts
             r   Úhits_due_for_bannerr­   s  s»   € ð €K€K€Kå˜4Ñ Ô €EØð Øˆ	Ø
�)Š)‰+Œ+€CÝÑ Ô €EØ�L 4Ñ'Ñ(€Fà€CØð )ð )ˆØ�yŠy˜œœ¨#Ñ.Ô.ˆØ�&Š=ˆ=Ø�JŠJ�s‰OŒOˆOØ%(ˆE�#”,”/Ñ"øØ
ð #Ý˜EÑ"Ô"Ð"Ø€Jr   útuple[bool, list[str]]c                óÖ   — t          | ¦  «        }|sddgfS g }t          |¦  «        D ]>\  }}|r|                     d¦  «         |                     t	          |¦  «        ¦  «         Œ?d|fS )z³Render the security-advisory section for ``hermes doctor``.

    Returns ``(has_problems, lines)``. Caller is responsible for printing
    with whatever color scheme it uses.
    Fu#   No active security advisories.  âœ“r   T)r_   rv   r8   Úextendrz   )r9   r¨   rr   rx   rt   s        r   Úrender_doctor_sectionr±   –  sŠ   € õ ˜4Ñ Ô €EØð >ØÐ<Ð=Ð=Ð=à€EÝ˜EÑ"Ô"ð 1ð 1‰ˆˆ3Øð 	Ø�LŠL˜ÑÔÐØ�ŠÕ*¨3Ñ/Ô/Ñ0Ô0Ð0Ð0Ø�ˆ;Ðr   c                óÊ   — t          | ¦  «        }|sdS t          |¦  «        }t          ¦   «         rd}d}|d                     |¦  «        z   |z   S d                     |¦  «        S )z²Return a printable startup banner, or None if nothing is due.

    Updates the banner cache as a side effect (so the next call within
    24h returns None for the same hit).
    Nz[1;31mz[0mrŸ   )r­   rs   rg   r¢   )r9   r«   rr   ÚredÚresets        r   Ústartup_bannerrµ   ¨  sq   € õ ˜dÑ
#Ô
#€CØð ØˆtÝ˜sÑ#Ô#€EÝÑÔð .ØˆØˆØ�T—Y’Y˜uÑ%Ô%Ñ%¨Ñ-Ð-Ø�9Š9�UÑÔÐr   c           
     ó4  — t          | ¦  «        }|sdS t          |¦  «        dk    rA|d         }d|j        j        › d|j        › d|j        › d|j        j        › d|j        j        › �
S t          |¦  «        › d	d
                     d„ |D ¦   «         ¦  «        › d�S )z=Return a one-line log message for gateway operators, or None.Nrk   r   zSecurity advisory [z
] active: rj   z	 matches z. See z" security advisories active (IDs: z, c              3  ó.   K  — | ]}|j         j        V — Œd S )Nr[   )rD   r\   s     r   ú	<genexpr>z&gateway_log_message.<locals>.<genexpr>Ä  s&   è è € Ð<Ð<°˜qœzœ}Ð<Ð<Ð<Ð<Ð<Ð<r   z7). Run `hermes doctor` on the gateway host for details.)	r_   ro   r#   r   r$   r%   r   r   r¢   )r9   r¨   r\   s      r   Úgateway_log_messager¹   ¹  sÝ   € å˜4Ñ Ô €EØð ØˆtÝ
ˆ5�z„z�Q‚€Ø�!ŒHˆð( a¤j¤mð (ð (Ø”9ð(ð (Ø !Ô 3ð(ð (Ø>?¼jÔ>Nð(ð (à”z”~ð(ð (ð 	)õ �5‰zŒzð Dð DØ—Y’YÐ<Ð<°eÐ<Ñ<Ô<Ñ<Ô<ðDð Dð Dð Er   )r&   r   r'   r(   )r4   r5   r'   r6   )r'   r=   )rP   r   r'   rQ   )r9   r6   r'   r6   )r'   rQ   )r9   r6   r'   rh   )rt   r"   r'   rh   )r'   r}   )r'   rˆ   )r™   rˆ   r'   rš   )r9   r6   r¤   r¥   r'   r6   )r9   r6   r'   r®   )r9   r6   r'   r(   )&r   Ú
__future__r   Úloggingrb   rd   Údataclassesr   Úpathlibr   Útypingr   r   Ú	getLoggerr   r1   r
   Ú	frozensetr    r   r"   r3   r<   rO   rX   r_   rg   rs   rz   r…   Ú_BANNER_REPEAT_HOURSr‡   r˜   r£   r­   r±   rµ   r¹   r   r   r   ú<module>rÂ      sè  ððð ð ðB #Ð "Ð "Ð "Ð "Ð "à €€€Ø 	€	€	€	Ø 
€
€
€
Ø !Ð !Ð !Ð !Ð !Ð !Ø Ð Ð Ð Ð Ð Ø %Ð %Ð %Ð %Ð %Ð %Ð %Ð %à	ˆÔ	˜8Ñ	$Ô	$€ð. €�$ÐÑÔðð ð ð ð ñ ô ñ Ôðð: €HØØLð	8ð @à˜)˜) W IÑ.Ô.Ð/ð
ð

ð Øð? ñ  ô  ð"$€
ð "ð "ð "ñ "ðT €�$ÐÑÔðð ð ð ð ñ ô ñ Ôððð ð ð ð, &0ðð ð ð ð ðF;ð ;ð ;ð ;ð(ð ð ð ð:;ð ;ð ;ð ;ðð ð ð ðð ð ð ð(ð ð ð ð> ,Ð ØÐ ðð ð ð ðð ð ð ð.Mð Mð Mð Mð -ðð ð ð ð ð ðFð ð ð ð$ð ð ð ð"Eð Eð Eð Eð Eð Er   