
    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     7/home/ubuntu/.hermes/hermes-agent/tools/skills_guard.pyr   r   <   sT         OOOMMMMMM
III
IIIJJJ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         OOOKKKLLL#eD999Hd7m999JGS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 URL2         >   .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Zg|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   )startx   Nu   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MM$E55D :I  5h+ a000 	 	GAtQx4y$66 #q"""#zz|||$$s**#/#5#=L"%%!& +! ! !   	& U!,,,  4# 	 	Dt||.t44	2#(!<s4yy<<<	<<< ky 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 	7Ayy{{ 7!--
3344##Ia$5$5666	7 
				 DIj*/BBCCC ..GZg|TTG<--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     2N;4OPPF			6>1	-	-BbzH7W!3WWfnWWWWW 
1v~ 1 1FO$$1 1 1
 	

 50f&8 0 0FN 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        rd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@RSTS]_`@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	LLp&+ppv}ppv?Qpp_nppqqq &'aJJ 6a6a6a6abbb  	D 	DA*""$$**1--C*""2&&CV&&af&&,,R00CLLBcBBCBB#BB!'#2#,BBBCCCCR*622OGV$	%	LL3f3363344499U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yy{{ HHQ\\^^,,,,   H	 
				 *	&&(())))Q[[]]3B3')))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yy{{ 	1<<>> 	!--	**++a
 <<>> 	99;;..y/@/@/B/BCC 	OOG#3!+!, 6H66$P% % %     	 	 	/%(* <! ! !     	 	6688#D$JJ 	 	 	H	 $t+++OOG+!%)))Wtt|WW@RWWW      hnn...OOG(#%&&&VsVVV      ;;;@PSX@X;OOG2!%*`      N""'!'''QZ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& 99T/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4S4Sy   ''00 	4E4P4P99 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BAqzZ/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)      ::A1:'::::::r&   r   r   )any)r/   has_criticalhas_highs      r'   r-  r-    sf     vBBBBBBBL:::::::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2AQZ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2JaagaaCMMaa$))FS]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
 
 qq99                R R Rj              2> > > >d7m > > > >B, ,4 , ,z , , , ,^" " "D "U4QT9EU " " " "J!z !c ! ! ! !H*T *c * * * *&r rg r r r rj1S 1S 1 1 1 1:     6g 3    b bc b# b btT[} bad b b b b b br&   