§
    ‰eîi	�  ã                   óú  — d Z ddlZddlZddlmZmZ ddlmZmZ ddlm	Z	 ddl
mZmZ ddhZd	d
dddœZddddœZe G d„ d¦  «        ¦   «         Ze G d„ d¦  «        ¦   «         Zg d¢ZdZdZdZh d£Zh d£Zh d£Zd6de	dedee         fd „Zd7d"e	d#edefd$„Zd8d&ed'edeeef         fd(„Zd&edefd)„Zd"e	defd*„Zd+e	dee         fd,„Z d-edefd.„Z!d#edefd/„Z"d0ee         defd1„Z#d2ed#ed3ed4ed0ee         defd5„Z$dS )9uÇ  
Skills Guard â€” Security scanner for externally-sourced skills.

Every skill downloaded from a registry passes through this scanner before
installation. It uses regex-based static analysis to detect known-bad patterns
(data exfiltration, prompt injection, destructive commands, persistence, etc.)
and a trust-aware install policy that determines whether a skill is allowed
based on both the scan verdict and the source's trust level.

Trust levels:
  - builtin:   Ships with Hermes. Never scanned, always trusted.
  - trusted:   openai/skills and anthropics/skills only. Caution verdicts allowed.
  - community: Everything else. Any findings = blocked unless --force.

Usage:
    from tools.skills_guard import scan_skill, should_allow_install, format_scan_report

    result = scan_skill(Path("skills/.hub/quarantine/some-skill"), source="community")
    allowed, reason = should_allow_install(result)
    if not allowed:
        print(format_scan_report(result))
é    N)Ú	dataclassÚfield)ÚdatetimeÚtimezone)ÚPath)ÚListÚTuplezopenai/skillszanthropics/skills)Úallowr
   r
   )r
   r
   Úblock)r
   r   r   )r
   r
   Úask)ÚbuiltinÚtrustedÚ	communityúagent-createdé   é   )ÚsafeÚcautionÚ	dangerousc                   óV   — e Zd ZU eed<   eed<   eed<   eed<   eed<   eed<   eed<   dS )	ÚFindingÚ
pattern_idÚseverityÚcategoryÚfileÚlineÚmatchÚdescriptionN)Ú__name__Ú
__module__Ú__qualname__ÚstrÚ__annotations__Úint© ó    ú4/home/div/.hermes/hermes-agent/tools/skills_guard.pyr   r   <   sT   € € € € € € à€O€O�OØ€M€M�MØ€M€M�MØ
€I€I�IØ
€I€I�IØ€J€J�JØÐÐÑÐÐr&   r   c                   ó‚   — e Zd ZU eed<   eed<   eed<   eed<    ee¬¦  «        Zee	         ed<   dZ
eed<   dZeed	<   d
S )Ú
ScanResultÚ
skill_nameÚsourceÚtrust_levelÚverdict)Údefault_factoryÚfindingsÚ Ú
scanned_atÚsummaryN)r   r    r!   r"   r#   r   Úlistr/   r   r   r1   r2   r%   r&   r'   r)   r)   G   sz   € € € € € € à€O€O�OØ€K€K�KØÐÐÑØ€L€L�LØ#˜e°DÐ9Ñ9Ô9€Hˆd�7ŒmÐ9Ð9Ñ9Ø€J�ÐÐÑØ€GˆSÐÐÑÐÐr&   r)   )x)z?curl\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)Úenv_exfil_curlÚcriticalÚexfiltrationz6curl command interpolating secret environment variable)z?wget\s+[^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL|API)Úenv_exfil_wgetr5   r6   z6wget command interpolating secret environment variable)z7fetch\s*\([^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|API)Úenv_exfil_fetchr5   r6   z6fetch() call interpolating secret environment variable)zBhttpx?\.(get|post|put|patch)\s*\([^\n]*(KEY|TOKEN|SECRET|PASSWORD)Úenv_exfil_httpxr5   r6   z&HTTP library call with secret variable)zDrequests\.(get|post|put|patch)\s*\([^\n]*(KEY|TOKEN|SECRET|PASSWORD)Úenv_exfil_requestsr5   r6   z*requests library call with secret variable)zbase64[^\n]*envÚencoded_exfilÚhighr6   z0base64 encoding combined with environment access)z\$HOME/\.ssh|\~/\.sshÚssh_dir_accessr<   r6   zreferences user SSH directory)z\$HOME/\.aws|\~/\.awsÚaws_dir_accessr<   r6   z)references user AWS credentials directory)z\$HOME/\.gnupg|\~/\.gnupgÚgpg_dir_accessr<   r6   zreferences user GPG keyring)z\$HOME/\.kube|\~/\.kubeÚkube_dir_accessr<   r6   z&references Kubernetes config directory)z\$HOME/\.docker|\~/\.dockerÚdocker_dir_accessr<   r6   z5references Docker config (may contain registry creds))z'\$HOME/\.hermes/\.env|\~/\.hermes/\.envÚhermes_env_accessr5   r6   z'directly references Hermes secrets file)zAcat\s+[^\n]*(\.env|credentials|\.netrc|\.pgpass|\.npmrc|\.pypirc)Úread_secrets_filer5   r6   zreads known secrets file)zprintenv|env\s*\|Údump_all_envr<   r6   zdumps all environment variables)z*os\.environ\b(?!\s*\.get\s*\(\s*["\']PATH)Úpython_os_environr<   r6   z(accesses os.environ (potential env dump))z@os\.getenv\s*\(\s*[^\)]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)Úpython_getenv_secretr5   r6   zreads secret via os.getenv())zprocess\.env\[Únode_process_envr<   r6   z*accesses process.env (Node.js environment))z$ENV\[.*(?:KEY|TOKEN|SECRET|PASSWORD)Úruby_env_secretr5   r6   zreads secret via Ruby ENV[])z \b(dig|nslookup|host)\s+[^\n]*\$Ú	dns_exfilr5   r6   zBDNS lookup with variable interpolation (possible DNS exfiltration))z,>\s*/tmp/[^\s]*\s*&&\s*(curl|wget|nc|python)Útmp_stagingr5   r6   zwrites to /tmp then exfiltrates)z!\[.*\]\(https?://[^\)]*\$\{?Úmd_image_exfilr<   r6   zBmarkdown image URL with variable interpolation (image-based exfil))z\[.*\]\(https?://[^\)]*\$\{?Úmd_link_exfilr<   r6   z)markdown link with variable interpolation)z=ignore\s+(?:\w+\s+)*(previous|all|above|prior)\s+instructionsÚprompt_injection_ignorer5   Ú	injectionz.prompt injection: ignore previous instructions)zyou\s+are\s+(?:\w+\s+)*now\s+Úrole_hijackr<   rN   z%attempts to override the agent's role)z2do\s+not\s+(?:\w+\s+)*tell\s+(?:\w+\s+)*the\s+userÚdeception_hider5   rN   z-instructs agent to hide information from user)zsystem\s+prompt\s+overrideÚsys_prompt_overrider5   rN   z&attempts to override the system prompt)z+pretend\s+(?:\w+\s+)*(you\s+are|to\s+be)\s+Úrole_pretendr<   rN   z6attempts to make the agent assume a different identity)zRdisregard\s+(?:\w+\s+)*(your|all|any)\s+(?:\w+\s+)*(instructions|rules|guidelines)Údisregard_rulesr5   rN   z&instructs agent to disregard its rules)z-output\s+(?:\w+\s+)*(system|initial)\s+promptÚleak_system_promptr<   rN   z%attempts to extract the system prompt)z.(when|if)\s+no\s*one\s+is\s+(watching|looking)Úconditional_deceptionr<   rN   z=conditional instruction to behave differently when unobserved)zwact\s+as\s+(if|though)\s+(?:\w+\s+)*you\s+(?:\w+\s+)*(have\s+no|don\'t\s+have)\s+(?:\w+\s+)*(restrictions|limits|rules)Úbypass_restrictionsr5   rN   z+instructs agent to act without restrictions)z5translate\s+.*\s+into\s+.*\s+and\s+(execute|run|eval)Útranslate_executer5   rN   z(translate-then-execute evasion technique)z9<!--[^>]*(?:ignore|override|system|secret|hidden)[^>]*-->Úhtml_comment_injectionr<   rN   z$hidden instructions in HTML comments)z5<\s*div\s+style\s*=\s*["\'][\s\S]*?display\s*:\s*noneÚ
hidden_divr<   rN   z(hidden HTML div (invisible instructions))zrm\s+-rf\s+/Údestructive_root_rmr5   Údestructivezrecursive delete from root)z+rm\s+(-[^\s]*)?r.*\$HOME|\brmdir\s+.*\$HOMEÚdestructive_home_rmr5   r[   z)recursive delete targeting home directory)zchmod\s+777Úinsecure_permsÚmediumr[   zsets world-writable permissions)z	>\s*/etc/Úsystem_overwriter5   r[   z$overwrites system configuration file)z\bmkfs\bÚformat_filesystemr5   r[   zformats a filesystem)z\bdd\s+.*if=.*of=/dev/Údisk_overwriter5   r[   zraw disk write operation)zshutil\.rmtree\s*\(\s*[\"\'/]Úpython_rmtreer<   r[   z/Python rmtree on absolute or root-relative path)ztruncate\s+-s\s*0\s+/Útruncate_systemr5   r[   z#truncates system file to zero bytes)z\bcrontab\bÚpersistence_cronr^   Úpersistencezmodifies cron jobs)zB\.(bashrc|zshrc|profile|bash_profile|bash_login|zprofile|zlogin)\bÚshell_rc_modr^   re   zreferences shell startup file)Úauthorized_keysÚssh_backdoorr5   re   zmodifies SSH authorized keys)z
ssh-keygenÚ
ssh_keygenr^   re   zgenerates SSH keys)z-systemd.*\.service|systemctl\s+(enable|start)Úsystemd_servicer^   re   z%references or enables systemd service)z/etc/init\.d/Úinit_scriptr^   re   z references init.d startup script)z+launchctl\s+load|LaunchAgents|LaunchDaemonsÚmacos_launchdr^   re   z%macOS launch agent/daemon persistence)z/etc/sudoers|visudoÚsudoers_modr5   re   z'modifies sudoers (privilege escalation))zgit\s+config\s+--global\s+Úgit_config_globalr^   re   z!modifies global git configuration)z#\bnc\s+-[lp]|ncat\s+-[lp]|\bsocat\bÚreverse_shellr5   Únetworkz potential reverse shell listener)z4\bngrok\b|\blocaltunnel\b|\bserveo\b|\bcloudflared\bÚtunnel_servicer<   rp   z*uses tunneling service for external access)z*\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}:\d{2,5}Úhardcoded_ip_portr^   rp   zhardcoded IP address with port)z0\.0\.0\.0:\d+|INADDR_ANYÚbind_all_interfacesr<   rp   zbinds to all network interfaces)z /bin/(ba)?sh\s+-i\s+.*>/dev/tcp/Úbash_reverse_shellr5   rp   z+bash interactive reverse shell via /dev/tcp)z'python[23]?\s+-c\s+["\']import\s+socketÚpython_socket_onelinerr5   rp   z9Python one-liner socket connection (likely reverse shell))zsocket\.connect\s*\(\s*\(Úpython_socket_connectr<   rp   z'Python socket connect to arbitrary host)z9webhook\.site|requestbin\.com|pipedream\.net|hookbin\.comÚexfil_servicer<   rp   z:references known data exfiltration/webhook testing service)z&pastebin\.com|hastebin\.com|ghostbin\.Úpaste_servicer^   rp   z0references paste service (possible data staging))zbase64\s+(-d|--decode)\s*\|Úbase64_decode_piper<   Úobfuscationz%base64 decodes and pipes to execution)z7\\x[0-9a-fA-F]{2}.*\\x[0-9a-fA-F]{2}.*\\x[0-9a-fA-F]{2}Úhex_encoded_stringr^   rz   z)hex-encoded string (possible obfuscation))z\beval\s*\(\s*["\']Úeval_stringr<   rz   zeval() with string argument)z\bexec\s*\(\s*["\']Úexec_stringr<   rz   zexec() with string argument)z1echo\s+[^\n]*\|\s*(bash|sh|python|perl|ruby|node)Úecho_pipe_execr5   rz   z'echo piped to interpreter for execution)z?compile\s*\(\s*[^\)]+,\s*["\'].*["\']\s*,\s*["\']exec["\']\s*\)Úpython_compile_execr<   rz   zPython compile() with exec mode)zgetattr\s*\(\s*__builtins__Úpython_getattr_builtinsr<   rz   z5dynamic access to Python builtins (evasion technique))z#__import__\s*\(\s*["\']os["\']\s*\)Úpython_import_osr<   rz   zdynamic import of os module)zcodecs\.decode\s*\(\s*["\']Úpython_codecs_decoder^   rz   z6codecs.decode (possible ROT13 or encoding obfuscation))zString\.fromCharCode|charCodeAtÚjs_char_coder^   rz   z=JavaScript character code construction (possible obfuscation))zatob\s*\(|btoa\s*\(Ú	js_base64r^   rz   zJavaScript base64 encode/decode)z\[::-1\]Ústring_reversalÚlowrz   z-string reversal (possible obfuscated payload))z)chr\s*\(\s*\d+\s*\)\s*\+\s*chr\s*\(\s*\d+Úchr_buildingr<   rz   z.building string from chr() calls (obfuscation))z7\\u[0-9a-fA-F]{4}.*\\u[0-9a-fA-F]{4}.*\\u[0-9a-fA-F]{4}Úunicode_escape_chainr^   rz   z/chain of unicode escapes (possible obfuscation))z.subprocess\.(run|call|Popen|check_output)\s*\(Úpython_subprocessr^   Ú	executionzPython subprocess execution)zos\.system\s*\(Úpython_os_systemr<   rŠ   u)   os.system() â€” unguarded shell execution)zos\.popen\s*\(Úpython_os_popenr<   rŠ   u#   os.popen() â€” shell pipe execution)z%child_process\.(exec|spawn|fork)\s*\(Únode_child_processr<   rŠ   zNode.js child_process execution)zRuntime\.getRuntime\(\)\.exec\(Újava_runtime_execr<   rŠ   u'   Java Runtime.exec() â€” shell execution)z`[^`]*\$\([^)]+\)[^`]*`Úbacktick_subshellr^   rŠ   z)backtick string with command substitution)z\.\./\.\./\.\.Úpath_traversal_deepr<   Ú	traversalz+deep relative path traversal (3+ levels up))z	\.\./\.\.Úpath_traversalr^   r‘   z&relative path traversal (2+ levels up))z/etc/passwd|/etc/shadowÚsystem_passwd_accessr5   r‘   z references system password files)z/proc/self|/proc/\d+/Úproc_accessr<   r‘   z3references /proc filesystem (process introspection))z	/dev/shm/Údev_shmr^   r‘   z.references shared memory (common staging area))z.xmrig|stratum\+tcp|monero|coinhive|cryptonightÚcrypto_miningr5   Úminingzcryptocurrency mining reference)zhashrate|nonce.*difficultyÚmining_indicatorsr^   r—   z)possible cryptocurrency mining indicators)zcurl\s+[^\n]*\|\s*(ba)?shÚcurl_pipe_shellr5   Úsupply_chainz*curl piped to shell (download-and-execute))z"wget\s+[^\n]*-O\s*-\s*\|\s*(ba)?shÚwget_pipe_shellr5   rš   z*wget piped to shell (download-and-execute))zcurl\s+[^\n]*\|\s*pythonÚcurl_pipe_pythonr5   rš   z curl piped to Python interpreter)z#\s*///\s*script.*dependenciesÚpep723_inline_depsr^   rš   zAPEP 723 inline script metadata with dependencies (verify pinning))z pip\s+install\s+(?!-r\s)(?!.*==)Úunpinned_pip_installr^   rš   z#pip install without version pinning)znpm\s+install\s+(?!.*@\d)Úunpinned_npm_installr^   rš   z#npm install without version pinning)zuv\s+run\s+Úuv_runr^   rš   z/uv run (may auto-install unpinned dependencies))zD(curl|wget|httpx?\.get|requests\.get|fetch)\s*[\(]?\s*["\']https?://Úremote_fetchr^   rš   z"fetches remote resource at runtime)zgit\s+clone\s+Ú	git_cloner^   rš   z"clones a git repository at runtime)zdocker\s+pull\s+Údocker_pullr^   rš   zpulls a Docker image at runtime)z^allowed-tools\s*:Úallowed_tools_fieldr<   Úprivilege_escalationz7skill declares allowed-tools (pre-approves tool access))z\bsudo\bÚ
sudo_usager<   r¥   z uses sudo (privilege escalation))zsetuid|setgid|cap_setuidÚsetuid_setgidr5   r¥   z.setuid/setgid (privilege escalation mechanism))ÚNOPASSWDÚnopasswd_sudor5   r¥   z:NOPASSWD sudoers entry (passwordless privilege escalation))zchmod\s+[u+]?sÚsuid_bitr5   r¥   zsets SUID/SGID bit on a file)z0AGENTS\.md|CLAUDE\.md|\.cursorrules|\.clinerulesÚagent_config_modr5   re   zTreferences agent config files (could persist malicious instructions across sessions))z'\.hermes/config\.yaml|\.hermes/SOUL\.mdÚhermes_config_modr5   re   z.references Hermes configuration files directly)z \.claude/settings|\.codex/configÚother_agent_configr<   re   z*references other agent configuration files)zI(?:api[_-]?key|token|secret|password)\s*[=:]\s*["\'][A-Za-z0-9+/=_-]{20,}Úhardcoded_secretr5   Úcredential_exposurez,possible hardcoded API key, token, or secret)z(-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----Úembedded_private_keyr5   r¯   zembedded private key)z0ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{80,}Úgithub_token_leakedr5   r¯   z-GitHub personal access token in skill content)zsk-[A-Za-z0-9]{20,}Úopenai_key_leakedr5   r¯   z(possible OpenAI API key in skill content)zsk-ant-[A-Za-z0-9_-]{90,}Úanthropic_key_leakedr5   r¯   z+possible Anthropic API key in skill content)zAKIA[0-9A-Z]{16}Úaws_access_key_leakedr5   r¯   z"AWS access key ID in skill content)z"\bDAN\s+mode\b|Do\s+Anything\s+NowÚjailbreak_danr5   rN   z'DAN (Do Anything Now) jailbreak attempt)z"\bdeveloper\s+mode\b.*\benabled?\bÚjailbreak_dev_moder5   rN   z developer mode jailbreak attempt)z3hypothetical\s+scenario.*(?:ignore|bypass|override)Úhypothetical_bypassr<   rN   z1hypothetical scenario used to bypass restrictions)z$for\s+educational\s+purposes?\s+onlyÚeducational_pretextr^   rN   z9educational pretext often used to justify harmful content)zW(respond|answer|reply)\s+without\s+(?:\w+\s+)*(restrictions|limitations|filters|safety)Úremove_filtersr5   rN   z1instructs agent to respond without safety filters)z>you\s+have\s+been\s+(?:\w+\s+)*(updated|upgraded|patched)\s+toÚfake_updater<   rN   z3fake update/patch announcement (social engineering))z8new\s+policy|updated\s+guidelines|revised\s+instructionsÚfake_policyr^   rN   z8claims new policy/guidelines (may be social engineering))zh(include|output|print|send|share)\s+(?:\w+\s+)*(conversation|chat\s+history|previous\s+messages|context)Úcontext_exfilr<   r6   z4instructs agent to output/share conversation history)z6(send|post|upload|transmit)\s+.*\s+(to|at)\s+https?://Úsend_to_urlr<   r6   z%instructs agent to send data to a URLé2   é   é   >   ú.rú.jlú.jsú.mdú.tsú.cfgú.cssú.iniú.phpú.texú.txtú.xmlú.ymlú.confú.htmlú.jsonú.tomlú.yamlú.plú.pyú.rbú.shú.bash>   ú.soú.appú.binú.comú.datú.debú.dllú.dmgú.exeú.msiú.rpmú.dylib>   õ   â€‹õ   â€Œõ   â€�õ   â€ªõ   â€«õ   â€¬õ   â€­õ   â€®õ   â� õ   â�¢õ   â�£õ   â�¤õ   â�¦õ   â�§õ   â�¨õ   â�©õ   ï»¿r0   Ú	file_pathÚrel_pathÚreturnc                 ó’  — |s| j         }| j                             ¦   «         t          vr| j         dk    rg S 	 |                      d¬¦  «        }n# t
          t          f$ r g cY S w xY wg }|                     d¦  «        }t          ¦   «         }t          D ]º\  }}}}	}
t          |d¬¦  «        D ] \  }}||f|v rŒt          j        ||t          j        ¦  «        rt|                     ||f¦  «         |                     ¦   «         }t!          |¦  «        dk    r|dd	…         d
z   }|                     t%          |||	||||
¬¦  «        ¦  «         Œ¡Œ»t          |d¬¦  «        D ]f\  }}t&          D ]Y}||v rSt)          |¦  «        }|                     t%          ddd||dt+          |¦  «        d›d|› d�d|› d�¬¦  «        ¦  «          nŒZŒg|S )a  
    Scan a single file for threat patterns and invisible unicode characters.

    Args:
        file_path: Absolute path to the file
        rel_path: Relative path for display (defaults to file_path.name)

    Returns:
        List of findings (deduplicated per pattern per line)
    zSKILL.mdzutf-8)ÚencodingÚ
r   )Ústartéx   Néu   z...©r   r   r   r   r   r   r   Úinvisible_unicoder<   rN   úU+Ú04Xú (ú)zinvisible unicode character z! (possible text hiding/injection))ÚnameÚsuffixÚlowerÚSCANNABLE_EXTENSIONSÚ	read_textÚUnicodeDecodeErrorÚOSErrorÚsplitÚsetÚTHREAT_PATTERNSÚ	enumerateÚreÚsearchÚ
IGNORECASEÚaddÚstripÚlenÚappendr   ÚINVISIBLE_CHARSÚ_unicode_char_nameÚord)rõ   rö   Úcontentr/   ÚlinesÚseenÚpatternÚpidr   r   r   Úir   Úmatched_textÚcharÚ	char_names                   r'   Ú	scan_filer"    sW  € ð ð "Ø”>ˆàÔ×ÒÑÔÕ';Ð;Ð;À	ÄÐR\Ò@\Ð@\Øˆ	ðØ×%Ò%¨wÐ%Ñ7Ô7ˆˆøÝ¥Ð(ð ð ð Øˆ	ˆ	ˆ	ðøøøð €HØ�MŠM˜$ÑÔ€EÝ‰5Œ5€Dõ :Ið ð Ñ5ˆ��h ¨+Ý  ¨aÐ0Ñ0Ô0ð 	ð 	‰GˆAˆtØ�Qˆx˜4ÐÐØÝŒy˜ $­¬Ñ6Ô6ð Ø—’˜#˜q˜Ñ"Ô"Ð"Ø#Ÿzšz™|œ|�Ý�|Ñ$Ô$ sÒ*Ð*Ø#/°°°Ô#5¸Ñ#=�LØ—’¥Ø"Ø%Ø%Ø!ØØ&Ø +ð!ñ !ô !ñ ô ð øð	õ& ˜U¨!Ð,Ñ,Ô,ð ð ‰ˆˆ4Ý#ð 	ð 	ˆDØ�tˆ|ˆ|Ý.¨tÑ4Ô4�	Ø—’¥Ø2Ø#Ø(Ø!ØØ<�s 4™yœyÐ<Ð<Ð<°	Ð<Ð<Ð<Ø k¸yÐ kÐ kÐ kð!ñ !ô !ñ ô ð ð �ð øð €Os   ¸A ÁA%Á$A%r   Ú
skill_pathr+   c           
      óÆ  — | j         }t          |¦  «        }g }|                      ¦   «         r”|                     t	          | ¦  «        ¦  «         |                      d¦  «        D ][}|                     ¦   «         rEt          |                     | ¦  «        ¦  «        }|                     t          ||¦  «        ¦  «         Œ\n<|                      ¦   «         r(|                     t          | | j         ¦  «        ¦  «         t          |¦  «        }t          |||||¦  «        }t          |||||t          j        t          j        ¦  «                             ¦   «         |¬¦  «        S )aì  
    Scan all files in a skill directory for security threats.

    Performs:
    1. Structural checks (file count, total size, binary files, symlinks)
    2. Regex pattern matching on all text files
    3. Invisible unicode character detection

    Args:
        skill_path: Path to the skill directory (must contain SKILL.md)
        source: Source identifier for trust level resolution (e.g. "openai/skills")

    Returns:
        ScanResult with verdict, findings, and trust metadata
    Ú*)r*   r+   r,   r-   r/   r1   r2   )r  Ú_resolve_trust_levelÚis_dirÚextendÚ_check_structureÚrglobÚis_filer"   Úrelative_tor"  Ú_determine_verdictÚ_build_summaryr)   r   Únowr   ÚutcÚ	isoformat)	r#  r+   r*   r,   Úall_findingsÚfÚrelr-   r2   s	            r'   Ú
scan_skillr5  W  sX  € ð  ”€JÝ& vÑ.Ô.€Kà"$€Là×ÒÑÔð 
Dà×ÒÕ,¨ZÑ8Ô8Ñ9Ô9Ð9ð ×!Ò! #Ñ&Ô&ð 	7ð 	7ˆAØ�yŠy‰{Œ{ð 7Ý˜!Ÿ-š-¨
Ñ3Ô3Ñ4Ô4�Ø×#Ò#¥I¨a°Ñ$5Ô$5Ñ6Ô6Ð6øð	7ð 
×	Ò	Ñ	Ô	ð DØ×Ò�I j°*´/ÑBÔBÑCÔCÐCå  Ñ.Ô.€GÝ˜Z¨°¸gÀ|ÑTÔT€GåØØØØØÝ”<¥¤Ñ-Ô-×7Ò7Ñ9Ô9Øðñ ô ð r&   FÚresultÚforcec           	      óÚ  — t                                | j        t           d         ¦  «        }t                               | j        d¦  «        }||         }|dk    rdd| j        › d| j        › d�fS |r"dd| j        › d	t          | j        ¦  «        › d
�fS |dk    r*dd| j        › d| j        › dt          | j        ¦  «        › d
�fS dd| j        › d| j        › dt          | j        ¦  «        › d�fS )a  
    Determine whether a skill should be installed based on scan result and trust.

    Args:
        result: Scan result from scan_skill()
        force: If True, override blocked policy decisions for this scan result

    Returns:
        (allowed, reason) tuple
    r   r   r
   Tz	Allowed (z	 source, z	 verdict)zForce-installed despite z
 verdict (z
 findings)r   NzRequires confirmation (z
 source + z
 verdict, Fz	Blocked (z$ findings). Use --force to override.)ÚINSTALL_POLICYÚgetr,   ÚVERDICT_INDEXr-   r  r/   )r6  r7  ÚpolicyÚviÚdecisions        r'   Úshould_allow_installr?  †  sa  € õ ×Ò Ô 2µNÀ;Ô4OÑPÔP€FÝ	×	Ò	˜6œ>¨1Ñ	-Ô	-€BØ�bŒz€Hà�7ÒÐØÐW Ô!3ÐWÐW¸f¼nÐWÐWÐWÐWÐWàð 
Øð1 v¤~ð 1ð 1Ý�F”OÑ$Ô$ð1ð 1ð 1ð
ð 	
ð
 �5ÒÐàð0 fÔ&8ð 0ð 0ÀFÄNð 0ð 0Ý�6”?Ñ#Ô#ð0ð 0ð 0ð
ð 	
ð
 ð	F�FÔ&ð 	Fð 	F°&´.ð 	Fð 	FÝˆvŒÑÔð	Fð 	Fð 	Fðð r&   c                 óî  ‡— g }| j                              ¦   «         }|                     d| j        › d| j        › d| j        › d|› �¦  «         | j        rÓddddd	œŠt          | j        ˆfd
„¬¦  «        }|D ]›}|j                             ¦   «          	                    d¦  «        }|j
         	                    d¦  «        }|j        › d|j        › � 	                    d¦  «        }|                     d|› d|› d|› d|j        dd…         › d�	¦  «         Œœ|                     d¦  «         t          | ¦  «        \  }}	|du rd}
n|€d}
nd}
|                     d|
› d|	› �¦  «         d                     |¦  «        S )zŒ
    Format a scan result as a human-readable report string.

    Returns a compact multi-line report suitable for CLI or chat display.
    zScan: r  ú/z)  Verdict: r   r   r   é   )r5   r<   r^   r†   c                 ó:   •— ‰                      | j        d¦  «        S )Né   )r:  r   )r3  Úseverity_orders    €r'   ú<lambda>z$format_scan_report.<locals>.<lambda>¹  s   ø€ À×@RÒ@RÐSTÔS]Ð_`Ñ@aÔ@a€ r&   )Úkeyé   é   ú:é   z  Ú z "Né<   ú"r0   TÚALLOWEDzNEEDS CONFIRMATIONÚBLOCKEDz
Decision: õ    â€” rú   )r-   Úupperr  r*   r+   r,   r/   Úsortedr   Úljustr   r   r   r   r?  Újoin)r6  r  Úverdict_displayÚsorted_findingsr3  ÚsevÚcatÚlocÚallowedÚreasonÚstatusrE  s              @r'   Úformat_scan_reportr^  «  sº  ø€ ð €Eà”n×*Ò*Ñ,Ô,€OØ	‡L‚LÐp˜&Ô+ÐpÐp¨v¬}ÐpÐp¸vÔ?QÐpÐpÐ_nÐpÐpÑqÔqÐqà„ð à&'°¸aÈÐJÐJˆÝ  ¤Ð6aÐ6aÐ6aÐ6aÐbÑbÔbˆà ð 	Dð 	DˆAØ”*×"Ò"Ñ$Ô$×*Ò*¨1Ñ-Ô-ˆCØ”*×"Ò" 2Ñ&Ô&ˆCØ”VÐ&Ð&˜aœfÐ&Ð&×,Ò,¨RÑ0Ô0ˆCØ�LŠLÐB˜cÐBÐB CÐBÐB¨#ÐBÐB°!´'¸#¸2¸#´,ÐBÐBÐBÑCÔCÐCÐCà�Š�RÑÔÐå*¨6Ñ2Ô2�O€GˆVØ�$€€ØˆˆØ	ˆØ%ˆˆàˆØ	‡L‚LÐ3˜fÐ3Ð3¨6Ð3Ð3Ñ4Ô4Ð4à�9Š9�UÑÔÐr&   c                 óê  — t          j        ¦   «         }|                      ¦   «         rst          |                      d¦  «        ¦  «        D ]O}|                     ¦   «         r9	 |                     |                     ¦   «         ¦  «         Œ?# t          $ r Y ŒKw xY wŒPn;|                      ¦   «         r'|                     |                      ¦   «         ¦  «         d| 	                    ¦   «         dd…         › �S )zPCompute a SHA-256 hash of all files in a skill directory for integrity tracking.r%  zsha256:Né   )
ÚhashlibÚsha256r'  rS  r*  r+  ÚupdateÚ
read_bytesr
  Ú	hexdigest)r#  Úhr3  s      r'   Úcontent_hashrg  Ï  s÷   € åŒÑÔ€AØ×ÒÑÔð *Ý˜
×(Ò(¨Ñ-Ô-Ñ.Ô.ð 	ð 	ˆAØ�yŠy‰{Œ{ð ðØ—H’H˜QŸ\š\™^œ^Ñ,Ô,Ð,Ð,øÝð ð ð Ø�Hðøøøðð	ð 
×	Ò	Ñ	Ô	ð *Ø	�Š�×&Ò&Ñ(Ô(Ñ)Ô)Ð)Ø)�Q—[’[‘]”] 3 B 3Ô'Ð)Ð)Ð)s   Á!'B	Â	
BÂBÚ	skill_dirc                 ó¤  — g }d}d}|                       d¦  «        D �]#}|                     ¦   «         s|                     ¦   «         sŒ,t          |                     | ¦  «        ¦  «        }|dz  }|                     ¦   «         r¤	 |                     ¦   «         }|                     |                      ¦   «         ¦  «        s,|                     t          ddd|dd|› �d¬	¦  «        ¦  «         n9# t          $ r, |                     t          d
dd|ddd¬	¦  «        ¦  «         Y nw xY w�Œ	 | 
                    ¦   «         j        }||z  }n# t          $ r Y �Œ8w xY w|t          dz  k    r>|                     t          ddd|d|dz  › d�d|dz  › dt          › d�¬	¦  «        ¦  «         |j                             ¦   «         }|t          v r0|                     t          ddd|dd|› �d|› d�¬	¦  «        ¦  «         |dvrE| 
                    ¦   «         j        dz  r)|                     t          ddd|ddd¬	¦  «        ¦  «         �Œ%|t"          k    r8|                     t          ddddd|› d �d!|› d"t"          › d#�¬	¦  «        ¦  «         |t$          dz  k    r>|                     t          d$d%ddd|dz  › d&�d'|dz  › d(t$          › d�¬	¦  «        ¦  «         |S ))a  
    Check the skill directory for structural anomalies:
    - Too many files
    - Suspiciously large total size
    - Binary/executable files that shouldn't be in a skill
    - Symlinks pointing outside the skill directory
    - Individual files that are too large
    r   r%  r   Úsymlink_escaper5   r‘   zsymlink -> z*symlink points outside the skill directoryrþ   Úbroken_symlinkr^   zbroken symlinkzbroken or circular symlinkr¿   Úoversized_fileÚ
structuralÚKBzfile is zKB (limit: zKB)Úbinary_filezbinary: zbinary/executable file (z) should not be in a skill)rÖ   r×   rÔ   rÕ   rÓ   éI   Úunexpected_executablezexecutable bit setzBfile has executable permission but is not a recognized script typeÚtoo_many_filesz(directory)z filesz
skill has z files (limit: r  Úoversized_skillr<   zKB totalz	skill is zKB total (limit: )r*  r+  Ú
is_symlinkr"   r,  ÚresolveÚis_relative_tor  r   r
  ÚstatÚst_sizeÚMAX_SINGLE_FILE_KBr  r  ÚSUSPICIOUS_BINARY_EXTENSIONSÚst_modeÚMAX_FILE_COUNTÚMAX_TOTAL_SIZE_KB)	rh  r/   Ú
file_countÚ
total_sizer3  r4  ÚresolvedÚsizeÚexts	            r'   r)  r)  â  sÍ  € ð €HØ€JØ€Jà�_Š_˜SÑ!Ô!ð Kñ KˆØ�yŠy‰{Œ{ð 	 1§<¢<¡>¤>ð 	Øå�!—-’- 	Ñ*Ô*Ñ+Ô+ˆØ�a‰ˆ
ð �<Š<‰>Œ>ð 	ðØŸ9š9™;œ;�Ø×.Ò.¨y×/@Ò/@Ñ/BÔ/BÑCÔCð 	Ø—O’O¥GØ#3Ø!+Ø!,Ø ØØ6¨HÐ6Ð6Ø$Pð%ñ %ô %ñ ô ð øøõ ð 	ð 	ð 	Ø—’¥Ø/Ø%Ø(ØØØ*Ø <ð!ñ !ô !ñ ô ð ð ð ð	øøøñ ð	Ø—6’6‘8”8Ô#ˆDØ˜$ÑˆJˆJøÝð 	ð 	ð 	Ø‰Hð	øøøð Õ$ tÑ+Ò+Ð+Ø�OŠO�GØ+Ø!Ø%ØØØ ™Ð)Ð)Ð)ØW t¨t¡|ÐWÐWÕ@RÐWÐWÐWðñ ô ñ ô ð ð Œh�nŠnÑÔˆØÕ.Ð.Ð.Ø�OŠO�GØ(Ø#Ø%ØØØ& Ð&Ð&ØV°sÐVÐVÐVðñ ô ñ ô ð ð Ð;Ð;Ð;ÀÇÂÁÄÔ@PÐSXÑ@XÐ;Ø�OŠO�GØ2Ø!Ø%ØØØ*Ø`ðñ ô ñ ô ð ùð •NÒ"Ð"Ø�Š�Ø'ØØ!ØØØÐ'Ð'Ð'ØQ ZÐQÐQÅÐQÐQÐQð
ñ 
ô 
ñ 	ô 	ð 	ð Õ%¨Ñ,Ò,Ð,Ø�Š�Ø(ØØ!ØØØ 4Ñ'Ð1Ð1Ð1Ø_ J°$Ñ$6Ð_Ð_ÕIZÐ_Ð_Ð_ð
ñ 
ô 
ñ 	ô 	ð 	ð €Os%   ÂA'C,Ã,3D"Ä!D"Ä(EÅ
EÅEr   c                 óº   — i dd“dd“dd“dd“d	d
“dd“dd“dd“dd“dd“dd“dd“dd“dd“dd“dd “d!d"“}|                      | d#t          | ¦  «        d$›�¦  «        S )%z7Get a readable name for an invisible unicode character.rä   zzero-width spacerå   zzero-width non-joinerræ   zzero-width joinerrì   zword joinerrí   zinvisible timesrî   zinvisible separatorrï   zinvisible plusrô   zBOM/zero-width no-break spacerç   zLTR embeddingrè   zRTL embeddingré   zpop directionalrê   zLTR overriderë   zRTL overriderð   zLTR isolaterñ   zRTL isolaterò   zfirst strong isolateró   zpop directional isolater   r  )r:  r  )r   Únamess     r'   r  r  W  s  € ðØÐ$ðàÐ)ðð 	Ð%ðð 	�-ð	ð
 	Ð#ðð 	Ð'ðð 	Ð"ðð 	Ð1ðð 	�/ðð 	�/ðð 	Ð#ðð 	�.ðð 	�.ðð 	�-ðð 	�-ðð  	Ð(ð!ð" 	Ð+ð#€Eð& �9Š9�TÐ/¥ D¡	¤	Ð/Ð/Ð/Ñ0Ô0Ð0r&   c                 ó  — d}| }|D ]0}|                      |¦  «        r|t          |¦  «        d…         } nŒ1|dk    rdS |                      d¦  «        s|dk    rdS t          D ] }|                      |¦  «        s||k    r dS Œ!dS )	z)Map a source identifier to a trust level.)z
skills-sh/z
skills.sh/z	skils-sh/z	skils.sh/Nr   z	official/Úofficialr   r   r   )Ú
startswithr  ÚTRUSTED_REPOS)r+   Úprefix_aliasesÚnormalized_sourceÚprefixr   s        r'   r&  r&  t  sÏ   € ð€Nð ÐØ ð ð ˆØ×'Ò'¨Ñ/Ô/ð 	Ø 1µ#°f±+´+°,°,Ô ?ÐØˆEð	ð
 ˜OÒ+Ð+Øˆà×#Ò# KÑ0Ô0ð Ð4EÈÒ4SÐ4SØˆyå ð ð ˆØ×'Ò'¨Ñ0Ô0ð 	Ð4EÈÒ4PÐ4PØ�9�9ð 5Qàˆ;r&   r/   c                 ó‚   — | sdS t          d„ | D ¦   «         ¦  «        }t          d„ | D ¦   «         ¦  «        }|rdS |rdS dS )z6Determine the overall verdict from a list of findings.r   c              3   ó,   K  — | ]}|j         d k    V — ŒdS )r5   N©r   ©Ú.0r3  s     r'   ú	<genexpr>z%_determine_verdict.<locals>.<genexpr>”  s)   è è € ÐBÐB°A�q”z ZÒ/ÐBÐBÐBÐBÐBÐBr&   c              3   ó,   K  — | ]}|j         d k    V — ŒdS )r<   NrŽ  r�  s     r'   r‘  z%_determine_verdict.<locals>.<genexpr>•  s)   è è € Ð:Ð:¨A�1”: Ò'Ð:Ð:Ð:Ð:Ð:Ð:r&   r   r   )Úany)r/   Úhas_criticalÚhas_highs      r'   r-  r-  �  sf   € àð ØˆvåÐBÐB¸ÐBÑBÔBÑBÔB€LÝÐ:Ð:°Ð:Ñ:Ô:Ñ:Ô:€Hàð Øˆ{Øð ØˆyØˆ9r&   r  Útrustr-   c                 ó¶   — |s| › d�S t          d„ |D ¦   «         ¦  «        }| › d|› dt          |¦  «        › dd                     t          |¦  «        ¦  «        › �S )z,Build a one-line summary of the scan result.z!: clean scan, no threats detectedc              3   ó$   K  — | ]}|j         V — Œd S )N)r   r�  s     r'   r‘  z!_build_summary.<locals>.<genexpr>£  s$   è è € Ð2Ð2 A�Q”ZÐ2Ð2Ð2Ð2Ð2Ð2r&   z: rQ  z finding(s) in z, )r  r  rU  rS  )r  r+   r–  r-   r/   Ú
categoriess         r'   r.  r.  ž  st   € àð :ØÐ9Ð9Ð9Ð9åÐ2Ð2¨Ð2Ñ2Ô2Ñ2Ô2€JØÐaÐa�gÐaÐa¥C¨¡M¤MÐaÐaÀ$Ç)Â)ÍFÐS]ÑL^ÔL^ÑB_ÔB_ÐaÐaÐar&   )r0   )r   )F)%Ú__doc__r  ra  Údataclassesr   r   r   r   Úpathlibr   Útypingr   r	   rˆ  r9  r;  r   r)   r  r|  r}  ry  r  rz  r  r"   r"  r5  Úboolr?  r^  rg  r)  r  r&  r-  r.  r%   r&   r'   ú<module>rŸ     sM  ððð ð. 
€	€	€	Ø €€€Ø (Ð (Ð (Ð (Ð (Ð (Ð (Ð (Ø 'Ð 'Ð 'Ð 'Ð 'Ð 'Ð 'Ð 'Ø Ð Ð Ð Ð Ð Ø Ð Ð Ð Ð Ð Ð Ð ð !Ð"5Ð6€ð 4Ø3Ø3ð
 2ð
ð 
€ð  q°qÐ9Ð9€ð ðð ð ð ð ñ ô ñ „ðð ðð ð ð ð ñ ô ñ „ððRð Rð R€ðj €ØÐ ØÐ ðð ð Ð ð ð  ð  Ð ðð ð €ð2>ð >˜ð >¨ð >°d¸7´mð >ð >ð >ð >ðB,ð ,˜4ð ,¨ð ,¸zð ,ð ,ð ,ð ,ð^"ð " ð "°Dð "ÀUÈ4ÐQTÈ9ÔEUð "ð "ð "ð "ðJ!˜zð !¨cð !ð !ð !ð !ðH*˜Tð * cð *ð *ð *ð *ð&r ð r¨¨g¬ð rð rð rð rðj1˜Sð 1 Sð 1ð 1ð 1ð 1ð: ð ¨ð ð ð ð ð6  g¤ð °3ð ð ð ð ðb˜ð b cð b°#ð bÀð bÈtÐT[Ì}ð bÐadð bð bð bð bð bð br&   