
    j                     0   d 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mZ ddl	m
Z
 ddlmZmZ h d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d8de
dedee         fdZd9d!e
d"edefd#Zd:d%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;d*e
dee         fd+Z!d,edefd-Z"d.Z# e$e#          Z%d/hZ&d*e
fd0Z'd"edefd1Z(d2ee         defd3Z)d4ed"ed5ed6ed2ee         defd7Z*dS )<u  
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Tuple>   NVIDIA/skillsopenai/skillsanthropics/skillshuggingface/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   F   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   listr3   r   r   r5   r6   r)   r*   r+   r-   r-   Q   sz         OOOKKKLLL#eD999Hd7m999JGSr*   r-   )y)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_wgetr9   r:   z6wget command interpolating secret environment variable)z7fetch\s*\([^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|API)env_exfil_fetchr9   r:   z6fetch() call interpolating secret environment variable)zBhttpx?\.(get|post|put|patch)\s*\([^\n]*(KEY|TOKEN|SECRET|PASSWORD)env_exfil_httpxr9   r:   z&HTTP library call with secret variable)zDrequests\.(get|post|put|patch)\s*\([^\n]*(KEY|TOKEN|SECRET|PASSWORD)env_exfil_requestsr9   r:   z*requests library call with secret variable)zbase64[^\n]*envencoded_exfilhighr:   z0base64 encoding combined with environment access)z\$HOME/\.ssh|\~/\.sshssh_dir_accessr@   r:   zreferences user SSH directory)z\$HOME/\.aws|\~/\.awsaws_dir_accessr@   r:   z)references user AWS credentials directory)z\$HOME/\.gnupg|\~/\.gnupggpg_dir_accessr@   r:   zreferences user GPG keyring)z\$HOME/\.kube|\~/\.kubekube_dir_accessr@   r:   z&references Kubernetes config directory)z\$HOME/\.docker|\~/\.dockerdocker_dir_accessr@   r:   z5references Docker config (may contain registry creds))z'\$HOME/\.hermes/\.env|\~/\.hermes/\.envhermes_env_accessr9   r:   z'directly references Hermes secrets file)zFcat\s+(?!>)[^\n]*(\.env|credentials|\.netrc|\.pgpass|\.npmrc|\.pypirc)read_secrets_filer9   r:   zreads known secrets file)zprintenv|env\s*\|dump_all_envr@   r:   zdumps all environment variables)zYos\.environ\b(?!\s*\.get\s*\(\s*["\'](?![^"\']*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)))python_os_environr@   r:   z(accesses os.environ (potential env dump))zOos\.environ\s*\.get\s*\(\s*["\'][^"\']*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)python_environ_get_secretr9   r:   z!reads secret via os.environ.get())z@os\.getenv\s*\(\s*[^\)]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)python_getenv_secretr9   r:   zreads secret via os.getenv())zprocess\.env\[node_process_envr@   r:   z*accesses process.env (Node.js environment))z$ENV\[.*(?:KEY|TOKEN|SECRET|PASSWORD)ruby_env_secretr9   r:   zreads secret via Ruby ENV[])z \b(dig|nslookup|host)\s+[^\n]*\$	dns_exfilr9   r:   zBDNS lookup with variable interpolation (possible DNS exfiltration))z,>\s*/tmp/[^\s]*\s*&&\s*(curl|wget|nc|python)tmp_stagingr9   r:   zwrites to /tmp then exfiltrates)z!\[.*\]\(https?://[^\)]*\$\{?md_image_exfilr@   r:   zBmarkdown image URL with variable interpolation (image-based exfil))z\[.*\]\(https?://[^\)]*\$\{?md_link_exfilr@   r:   z)markdown link with variable interpolation)z=ignore\s+(?:\w+\s+)*(previous|all|above|prior)\s+instructionsprompt_injection_ignorer9   	injectionz.prompt injection: ignore previous instructions)zyou\s+are\s+(?:\w+\s+)*now\s+role_hijackr@   rS   z%attempts to override the agent's role)z2do\s+not\s+(?:\w+\s+)*tell\s+(?:\w+\s+)*the\s+userdeception_hider9   rS   z-instructs agent to hide information from user)z0system\s+(?:\w+\s+)*prompt\s+(?:\w+\s+)*overridesys_prompt_overrider9   rS   z&attempts to override the system prompt)z+pretend\s+(?:\w+\s+)*(you\s+are|to\s+be)\s+role_pretendr@   rS   z6attempts to make the agent assume a different identity)zRdisregard\s+(?:\w+\s+)*(your|all|any)\s+(?:\w+\s+)*(instructions|rules|guidelines)disregard_rulesr9   rS   z&instructs agent to disregard its rules)z-output\s+(?:\w+\s+)*(system|initial)\s+promptleak_system_promptr@   rS   z%attempts to extract the system prompt)z.(when|if)\s+no\s*one\s+is\s+(watching|looking)conditional_deceptionr@   rS   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_restrictionsr9   rS   z+instructs agent to act without restrictions)z5translate\s+.*\s+into\s+.*\s+and\s+(execute|run|eval)translate_executer9   rS   z(translate-then-execute evasion technique)z9<!--[^>]*(?:ignore|override|system|secret|hidden)[^>]*-->html_comment_injectionr@   rS   z$hidden instructions in HTML comments)z5<\s*div\s+style\s*=\s*["\'][\s\S]*?display\s*:\s*none
hidden_divr@   rS   z(hidden HTML div (invisible instructions))zrm\s+-rf\s+/destructive_root_rmr9   destructivezrecursive delete from root)z+rm\s+(-[^\s]*)?r.*\$HOME|\brmdir\s+.*\$HOMEdestructive_home_rmr9   r`   z)recursive delete targeting home directory)zchmod\s+777insecure_permsmediumr`   zsets world-writable permissions)z	>\s*/etc/system_overwriter9   r`   z$overwrites system configuration file)z\bmkfs\bformat_filesystemr9   r`   zformats a filesystem)z\bdd\s+.*if=.*of=/dev/disk_overwriter9   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_systemr9   r`   z#truncates system file to zero bytes)z\bcrontab\bpersistence_cronrc   persistencezmodifies cron jobs)zB\.(bashrc|zshrc|profile|bash_profile|bash_login|zprofile|zlogin)\bshell_rc_modrc   rj   zreferences shell startup file)authorized_keysssh_backdoorr9   rj   zmodifies SSH authorized keys)z
ssh-keygen
ssh_keygenrc   rj   zgenerates SSH keys)z-systemd.*\.service|systemctl\s+(enable|start)systemd_servicerc   rj   z%references or enables systemd service)z/etc/init\.d/init_scriptrc   rj   z references init.d startup script)z+launchctl\s+load|LaunchAgents|LaunchDaemonsmacos_launchdrc   rj   z%macOS launch agent/daemon persistence)z/etc/sudoers|visudosudoers_modr9   rj   z'modifies sudoers (privilege escalation))zgit\s+config\s+--global\s+git_config_globalrc   rj   z!modifies global git configuration)z#\bnc\s+-[lp]|ncat\s+-[lp]|\bsocat\breverse_shellr9   networkz potential reverse shell listener)z4\bngrok\b|\blocaltunnel\b|\bserveo\b|\bcloudflared\btunnel_servicer@   ru   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_portrc   ru   zhardcoded IP address with port)z0\.0\.0\.0:\d+|INADDR_ANYbind_all_interfacesr@   ru   zbinds to all network interfaces)z /bin/(ba)?sh\s+-i\s+.*>/dev/tcp/bash_reverse_shellr9   ru   z+bash interactive reverse shell via /dev/tcp)z'python[23]?\s+-c\s+["\']import\s+socketpython_socket_onelinerr9   ru   z9Python one-liner socket connection (likely reverse shell))zsocket\.connect\s*\(\s*\(python_socket_connectr@   ru   z'Python socket connect to arbitrary host)z9webhook\.site|requestbin\.com|pipedream\.net|hookbin\.comexfil_servicer@   ru   z:references known data exfiltration/webhook testing service)z&pastebin\.com|hastebin\.com|ghostbin\.paste_servicerc   ru   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_stringrc   r   z)hex-encoded string (possible obfuscation))z\beval\s*\(\s*["\']eval_stringr@   r   zeval() with string argument)z\bexec\s*\(\s*["\']exec_stringr@   r   zexec() with string argument)z1echo\s+[^\n]*\|\s*(bash|sh|python|perl|ruby|node)echo_pipe_execr9   r   z'echo piped to interpreter for execution)z?compile\s*\(\s*[^\)]+,\s*["\'].*["\']\s*,\s*["\']exec["\']\s*\)python_compile_execr@   r   zPython compile() with exec mode)zgetattr\s*\(\s*__builtins__python_getattr_builtinsr@   r   z5dynamic access to Python builtins (evasion technique))z#__import__\s*\(\s*["\']os["\']\s*\)python_import_osr@   r   zdynamic import of os module)zcodecs\.decode\s*\(\s*["\']python_codecs_decoderc   r   z6codecs.decode (possible ROT13 or encoding obfuscation))zString\.fromCharCode|charCodeAtjs_char_coderc   r   z=JavaScript character code construction (possible obfuscation))zatob\s*\(|btoa\s*\(	js_base64rc   r   zJavaScript base64 encode/decode)z\[::-1\]string_reversallowr   z-string reversal (possible obfuscated payload))z)chr\s*\(\s*\d+\s*\)\s*\+\s*chr\s*\(\s*\d+chr_buildingr@   r   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_chainrc   r   z/chain of unicode escapes (possible obfuscation))z.subprocess\.(run|call|Popen|check_output)\s*\(python_subprocessrc   	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_subshellrc   r   z)backtick string with command substitution)z\.\./\.\./\.\.path_traversal_deepr@   	traversalz+deep relative path traversal (3+ levels up))z	\.\./\.\.path_traversalrc   r   z&relative path traversal (2+ levels up))z/etc/passwd|/etc/shadowsystem_passwd_accessr9   r   z references system password files)z/proc/self|/proc/\d+/proc_accessr@   r   z3references /proc filesystem (process introspection))z	/dev/shm/dev_shmrc   r   z.references shared memory (common staging area))z.xmrig|stratum\+tcp|monero|coinhive|cryptonightcrypto_miningr9   miningzcryptocurrency mining reference)zhashrate|nonce.*difficultymining_indicatorsrc   r   z)possible cryptocurrency mining indicators)zcurl\s+[^\n]*\|\s*(ba)?shcurl_pipe_shellr9   supply_chainz*curl piped to shell (download-and-execute))z"wget\s+[^\n]*-O\s*-\s*\|\s*(ba)?shwget_pipe_shellr9   r   z*wget piped to shell (download-and-execute))zcurl\s+[^\n]*\|\s*pythoncurl_pipe_pythonr9   r   z curl piped to Python interpreter)z#\s*///\s*script.*dependenciespep723_inline_depsrc   r   zAPEP 723 inline script metadata with dependencies (verify pinning))z pip\s+install\s+(?!-r\s)(?!.*==)unpinned_pip_installrc   r   z#pip install without version pinning)znpm\s+install\s+(?!.*@\d)unpinned_npm_installrc   r   z#npm install without version pinning)zuv\s+run\s+uv_runrc   r   z/uv run (may auto-install unpinned dependencies))zD(curl|wget|httpx?\.get|requests\.get|fetch)\s*[\(]?\s*["\']https?://remote_fetchrc   r   z"fetches remote resource at runtime)zgit\s+clone\s+	git_clonerc   r   z"clones a git repository at runtime)zdocker\s+pull\s+docker_pullrc   r   zpulls a Docker image at runtime)z^allowed-tools\s*:allowed_tools_fieldr   privilege_escalationzBskill declares allowed-tools (standard frontmatter; informational))z\bsudo\b
sudo_usager@   r   z uses sudo (privilege escalation))zsetuid|setgid|cap_setuidsetuid_setgidr9   r   z.setuid/setgid (privilege escalation mechanism))NOPASSWDnopasswd_sudor9   r   z:NOPASSWD sudoers entry (passwordless privilege escalation))zchmod\s+[u+]?ssuid_bitr9   r   zsets SUID/SGID bit on a file)z0AGENTS\.md|CLAUDE\.md|\.cursorrules|\.clinerulesagent_config_modr9   rj   zTreferences agent config files (could persist malicious instructions across sessions))z'\.hermes/config\.yaml|\.hermes/SOUL\.mdhermes_config_modr9   rj   z.references Hermes configuration files directly)z \.claude/settings|\.codex/configother_agent_configr@   rj   z*references other agent configuration files)zI(?:api[_-]?key|token|secret|password)\s*[=:]\s*["\'][A-Za-z0-9+/=_-]{20,}hardcoded_secretr9   credential_exposurez,possible hardcoded API key, token, or secret)z(-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----embedded_private_keyr9   r   zembedded private key)z0ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{80,}github_token_leakedr9   r   z-GitHub personal access token in skill content)zsk-[A-Za-z0-9]{20,}openai_key_leakedr9   r   z(possible OpenAI API key in skill content)zsk-ant-[A-Za-z0-9_-]{90,}anthropic_key_leakedr9   r   z+possible Anthropic API key in skill content)zAKIA[0-9A-Z]{16}aws_access_key_leakedr9   r   z"AWS access key ID in skill content)z"\bDAN\s+mode\b|Do\s+Anything\s+Nowjailbreak_danr9   rS   z'DAN (Do Anything Now) jailbreak attempt)z"\bdeveloper\s+mode\b.*\benabled?\bjailbreak_dev_moder9   rS   z developer mode jailbreak attempt)z3hypothetical\s+scenario.*(?:ignore|bypass|override)hypothetical_bypassr@   rS   z1hypothetical scenario used to bypass restrictions)z$for\s+educational\s+purposes?\s+onlyeducational_pretextrc   rS   z9educational pretext often used to justify harmful content)zW(respond|answer|reply)\s+without\s+(?:\w+\s+)*(restrictions|limitations|filters|safety)remove_filtersr9   rS   z1instructs agent to respond without safety filters)z>you\s+have\s+been\s+(?:\w+\s+)*(updated|upgraded|patched)\s+tofake_updater@   rS   z3fake update/patch announcement (social engineering))zYnew\s+(?:\w+\s+)*policy|updated\s+(?:\w+\s+)*guidelines|revised\s+(?:\w+\s+)*instructionsfake_policyrc   rS   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@   r:   z4instructs agent to output/share conversation history)z6(send|post|upload|transmit)\s+.*\s+(to|at)\s+https?://send_to_urlr@   r:   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>      ​   ‌   ‍   ‪   ‫   ‬   ‭   ‮   ⁠   ⁢   ⁣   ⁤   ⁦   ⁧   ⁨   ⁩   ﻿r4   	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)
    SKILL.mdutf-8encoding
r   )startx   Nu   z...r   r   r   r   r    r!   r"   invisible_unicoder@   rS   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   contentr3   linesseenpatternpidr   r   r"   ir    matched_textchar	char_names                   r+   	scan_filer*  2  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 }|                                 rt          |           }|                    t          | |                     |                     d          D ]g}|                                rQt          |	                    |                     } ||          rD|                    t          ||                     hn<|                                 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

    A skill may ship a `.skillignore` (or `.clawhubignore`) file with
    gitignore-style patterns. Matching paths are excluded from BOTH the
    structural checks and the pattern scan, so development/docs artifacts
    that are not part of the installed skill (e.g. `SKILL-original.md`,
    `docs/plans/`, `release-notes.md`) don't trip findings. The ignore
    file itself is always excluded. Patterns cannot un-ignore the
    skill's own `SKILL.md`, which is always scanned.

    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
    )ignore*)r.   r/   r0   r1   r3   r5   r6   )r  _resolve_trust_levelis_dir_load_skill_ignoreextend_check_structurerglobis_filer&   relative_tor*  _determine_verdict_build_summaryr-   r   nowr   utc	isoformat)
r+  r/   r.   r0   all_findingsr-  frelr1   r6   s
             r+   
scan_skillr?  s  s   0 J&v..K"$L D#J// 	,ZGGGHHH !!#&& 	7 	7Ayy{{ 7!--
33446#;; ##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           	      n   t                               | j        t           d                   }t                              | j        d          }||         }|dk    rdd| j         d| j         dfS |r6| j        dk    r	| j        d	v s"dd
| j         dt          | j                   dfS |dk    r*dd| j         d| j         dt          | j                   dfS | j        dk    r+| j        d	v r"d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)r   )r   r   zForce-installed despite z
 verdict (z
 findings)r   NzRequires confirmation (z
 source + z
 verdict, Fz	Blocked (z source + dangerous verdict, z: findings). --force does not override a dangerous verdict.z$ findings). Use --force to override.)INSTALL_POLICYgetr0   VERDICT_INDEXr1   r  r3   )r@  rA  policyvidecisions        r+   should_allow_installrI    s     2N;4OPPF			6>1	-	-BbzH7W!3WWfnWWWWW 
fn338JNf8f8f1v~ 1 1FO$$1 1 1
 	

 50f&8 0 0FN 0 06?##0 0 0
 	
 ~$$);?W)W)W`* ` `6?##` ` `
 	
 	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      )r9   r@   rc   r   c                 :                         | j        d          S )N   )rD  r   )r=  severity_orders    r+   <lambda>z$format_scan_report.<locals>.<lambda>  s    @R@RSTS]_`@a@a r*   )key      :   z   z "N<   "r4   TALLOWEDzNEEDS CONFIRMATIONBLOCKEDz
Decision:     — r  )r1   upperr  r.   r/   r0   r3   sortedr   ljustr   r   r    r!   rI  join)r@  r"  verdict_displaysorted_findingsr=  sevcatlocallowedreasonstatusrO  s              @r+   format_scan_reportrh    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                    }|                                 rt          |                     d                    D ]}|                                r	 |                    |                                           }|                    |	                    d                     |                    d           |                    |
                                           # t          $ r Y w xY wn;|                                 r'|                    | 
                                           d|                                dd          S )u  Compute a SHA-256 hash of all files in a skill directory for integrity tracking.

    File paths (relative to ``skill_path``) are mixed into the hash alongside
    file contents so that swapping the contents of two files in a skill
    changes the hash. This must stay symmetric with
    ``tools.skills_hub.bundle_content_hash`` — both functions need to
    produce the same digest for the same skill (one operates on disk,
    one on an in-memory bundle), so any change to the hash shape MUST
    land in both places at once.
    r.  r       zsha256:N   )hashlibsha256r0  r]  r4  r5  r6  as_posixupdateencode
read_bytesr  	hexdigest)r+  hr=  r>  s       r+   content_hashrt    sF    	A *
((--.. 	 	Ayy{{ --
33<<>>CHHSZZ00111HHW%%%HHQ\\^^,,,,   H	 
				 *	&&(())))Q[[]]3B3')))s   !BC--
C:9C:	skill_dirc                    |d }g }d}d}|                      d          D ]/}|                                s|                                s,t          |                    |                     } ||          rZ|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 Dw 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                     1|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 )+aW  
    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

    Args:
        skill_dir: Path to the skill directory.
        ignore: Optional callable taking a relative posix path and returning
            True if the path should be excluded (e.g. from `.skillignore`).
            Ignored files are not counted toward the file count, total size,
            or any structural finding.
    Nc                     dS )NFr)   )_rels    r+   rP  z"_check_structure.<locals>.<lambda>.  s    e r*   r   r.  r   symlink_escaper9   r   zsymlink -> z*symlink points outside the skill directoryr  broken_symlinkrc   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: )r4  r5  
is_symlinkr&   r6  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)
ru  r-  r3   
file_count
total_sizer=  r>  resolvedsizeexts
             r+   r3  r3    s     ~##HJJ__S!! M Myy{{ 	1<<>> 	!--	**++6#;; 	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==3D32D39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	  )rD  r   )r(  namess     r+   r  r    s   $) 	% 	-	
 	# 	' 	" 	1 	/ 	/ 	# 	. 	. 	- 	-  	(!" 	+#E& 99T/D		///000r*   )z.skillignorez.clawhubignorer   c                    g t           D ]}| |z  }	 |                                rl|                    d                                          D ]C}|                                }|r|                    d          r.                    |           D# t          t          f$ r Y w xY wdt          dt          ffd}|S )a0  Build a matcher from a skill's `.skillignore` / `.clawhubignore`.

    Returns a callable ``ignore(rel_posix_path) -> bool``. The matcher
    supports gitignore-style basics: blank lines and ``#`` comments are
    skipped, a trailing ``/`` marks a directory (matches that dir and
    everything under it), and ``*``/``?`` globs are honored via fnmatch on
    both the full relative path and each path segment. A leading ``/``
    anchors a pattern to the skill root. The ignore files themselves are
    always excluded; ``SKILL.md`` can never be excluded.
    r   r   #r>  r   c                 
   t          |                                           }|                    d          d         }|t          v rdS |t          v rdS D ]*}|                    d          }|                    d                              d          }                    d          sZ|rL|k    s|                    dz             r dS |s(d|z   dz   	                    dz   dz             dk    r dS t          j
        |          r dS |sht          j
        |          r dS dvr1t          fd|                    d          D                       r dS |                    dz             r dS ,dS )NrK  FTc              3   B   K   | ]}t          j         |          V  d S N)fnmatch).0segps     r+   	<genexpr>z5_load_skill_ignore.<locals>.ignore.<locals>.<genexpr>  s@       ( (03GOC++( ( ( ( ( (r*   )r   rn  r  _NEVER_IGNORABLE_ALWAYS_IGNORED_NAMES
startswithlstripendswithrstripfindr  any)r>  	rel_posixbasepatanchoredr0  r  patternss         @r+   r-  z"_load_skill_ignore.<locals>.ignore  s   II&&((	s##B'###5(((4 	  	 C~~c**H

3AZZ__FA  >>Y%9%9!c'%B%B>44  S9_s%:$@$@q3$O$OSU$U$U44 y!,, tt  ?4++  44a<<C ( ( ( (7@s7K7K( ( ( % %<  44 ''C00  44ur*   )_SKILL_IGNORE_FILENAMESr5  r  
splitlinesr  r  r  r  r  r&   bool)ru  r  igrawr    r-  r  s         @r+   r1  r1    s     H' 
 
	zz|| *<<<99DDFF * *C99;;D !4??3#7#7 ! OOD))))"G, 	 	 	H	(C (D ( ( ( ( ( (T Ms   B BB('B(c                     d}| }|D ]0}|                     |          r|t          |          d         } n1|dk    rdS |dk    rdS t          D ]#}||k    s|                     | d          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   officialr   rK  r   r   )r  r  TRUSTED_REPOS)r/   prefix_aliasesnormalized_sourceprefixr   s        r+   r/  r/    s    N    ''// 	 1#f++,, ?E	
 O++ J&&y !  ''+<+G+G7+V+V'99 (;r*   r3   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 )r9   Nr   r  r=  s     r+   r  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   )r  )r3   has_criticalhas_highs      r+   r7  r7  (  sf     vBBBBBBBL:::::::H { y6r*   r  trustr1   c                     |s|  dS 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                     h | ]	}|j         
S r)   )r   r  s     r+   	<setcomp>z!_build_summary.<locals>.<setcomp>=  s    ///!*///r*   z: r[  z finding(s) in z, )r  r_  r]  )r  r/   r  r1   r3   
categoriess         r+   r8  r8  8  sk     :9999//h///JaagaaCMMaa$))FS]L^L^B_B_aaar*   )r4   )r   )Fr  )+__doc__r  r  rl  dataclassesr   r   r   r   pathlibr   typingr   r	   r  rC  rE  r   r-   r  r  r  r  r  r  r  r&   r*  r?  r  rI  rh  rt  r3  r  r  r  r  r  r1  r/  r7  r8  r)   r*   r+   <module>r     s   . 
			   ( ( ( ( ( ( ( ( ' ' ' ' ' ' ' '              	 	 	 433
 2
 
 qq99                d d dN              2> > > >d7m > > > >B8 84 8 8z 8 8 8 8v) ) )D )U4QT9EU ) ) ) )X!z !c ! ! ! !H*T *c * * * *>~ ~ ~d7m ~ ~ ~ ~B1S 1S 1 1 1 1@ =  344 < B$ B B B BJ     :g 3     b bc b# b btT[} bad b b b b b br*   