
    nSj                        d Z ddlZddlZddlZddlZddlmZmZ ddlmZm	Z	 ddl
mZ ddlmZmZ d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dAdededee         fd ZdBd"ed#edefd$Zd"edefd%Zd"edefd&Z d'ede!fd(Z"	 dBddd)d"ed#ed*ed+edz  deee!f         f
d,Z#dCd.ed/e$dee$ef         fd0Z%d.edefd1Z&d"edefd2Z'dDd3edee         fd4Z(d5edefd6Z)d7Z* e+e*          Z,d8hZ-d3efd9Z.d#edefd:Z/d;ee         defd<Z0d=ed#ed>ed?ed;ee         defd@Z1dS )Eu  
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skills-guard-v1>   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     4/home/ice/.hermes/hermes-agent/tools/skills_guard.pyr   r   J   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	<    ee          Zeed
<   dS )
ScanResult
skill_namesourcetrust_levelverdict)default_factoryfindings 
scanned_atsummaryscan_provenanceN)r#   r$   r%   r&   r'   r   listr3   r   r   r5   r6   dictr7   r)   r*   r+   r-   r-   U   s         OOOKKKLLL#eD999Hd7m999JGS!E$777OT77777r*   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_wgetr;   r<   z6wget command interpolating secret environment variable)z7fetch\s*\([^\n]*\$\{?\w*(KEY|TOKEN|SECRET|PASSWORD|API)env_exfil_fetchr;   r<   z6fetch() call interpolating secret environment variable)zBhttpx?\.(get|post|put|patch)\s*\([^\n]*(KEY|TOKEN|SECRET|PASSWORD)env_exfil_httpxr;   r<   z&HTTP library call with secret variable)zDrequests\.(get|post|put|patch)\s*\([^\n]*(KEY|TOKEN|SECRET|PASSWORD)env_exfil_requestsr;   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_accessrB   r<   zreferences user SSH directory)z\$HOME/\.aws|\~/\.awsaws_dir_accessrB   r<   z)references user AWS credentials directory)z\$HOME/\.gnupg|\~/\.gnupggpg_dir_accessrB   r<   zreferences user GPG keyring)z\$HOME/\.kube|\~/\.kubekube_dir_accessrB   r<   z&references Kubernetes config directory)z\$HOME/\.docker|\~/\.dockerdocker_dir_accessrB   r<   z5references Docker config (may contain registry creds))z'\$HOME/\.hermes/\.env|\~/\.hermes/\.envhermes_env_accessr;   r<   z'directly references Hermes secrets file)zFcat\s+(?!>)[^\n]*(\.env|credentials|\.netrc|\.pgpass|\.npmrc|\.pypirc)read_secrets_filer;   r<   zreads known secrets file)zprintenv|env\s*\|dump_all_envrB   r<   zdumps all environment variables)zYos\.environ\b(?!\s*\.get\s*\(\s*["\'](?![^"\']*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)))python_os_environrB   r<   z(accesses os.environ (potential env dump))zOos\.environ\s*\.get\s*\(\s*["\'][^"\']*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)python_environ_get_secretr;   r<   z!reads secret via os.environ.get())z@os\.getenv\s*\(\s*[^\)]*(?:KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL)python_getenv_secretr;   r<   zreads secret via os.getenv())zprocess\.env\[node_process_envrB   r<   z*accesses process.env (Node.js environment))z$ENV\[.*(?:KEY|TOKEN|SECRET|PASSWORD)ruby_env_secretr;   r<   zreads secret via Ruby ENV[])z \b(dig|nslookup|host)\s+[^\n]*\$	dns_exfilr;   r<   zBDNS lookup with variable interpolation (possible DNS exfiltration))z,>\s*/tmp/[^\s]*\s*&&\s*(curl|wget|nc|python)tmp_stagingr;   r<   zwrites to /tmp then exfiltrates)z!\[.*\]\(https?://[^\)]*\$\{?md_image_exfilrB   r<   zBmarkdown image URL with variable interpolation (image-based exfil))z\[.*\]\(https?://[^\)]*\$\{?md_link_exfilrB   r<   z)markdown link with variable interpolation)z=ignore\s+(?:\w+\s+)*(previous|all|above|prior)\s+instructionsprompt_injection_ignorer;   	injectionz.prompt injection: ignore previous instructions)zyou\s+are\s+(?:\w+\s+)*now\s+role_hijackrB   rU   z%attempts to override the agent's role)z2do\s+not\s+(?:\w+\s+)*tell\s+(?:\w+\s+)*the\s+userdeception_hider;   rU   z-instructs agent to hide information from user)z0system\s+(?:\w+\s+)*prompt\s+(?:\w+\s+)*overridesys_prompt_overrider;   rU   z&attempts to override the system prompt)z+pretend\s+(?:\w+\s+)*(you\s+are|to\s+be)\s+role_pretendrB   rU   z6attempts to make the agent assume a different identity)zRdisregard\s+(?:\w+\s+)*(your|all|any)\s+(?:\w+\s+)*(instructions|rules|guidelines)disregard_rulesr;   rU   z&instructs agent to disregard its rules)z-output\s+(?:\w+\s+)*(system|initial)\s+promptleak_system_promptrB   rU   z%attempts to extract the system prompt)z.(when|if)\s+no\s*one\s+is\s+(watching|looking)conditional_deceptionrB   rU   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_restrictionsr;   rU   z+instructs agent to act without restrictions)z5translate\s+.*\s+into\s+.*\s+and\s+(execute|run|eval)translate_executer;   rU   z(translate-then-execute evasion technique)z9<!--[^>]*(?:ignore|override|system|secret|hidden)[^>]*-->html_comment_injectionrB   rU   z$hidden instructions in HTML comments)z5<\s*div\s+style\s*=\s*["\'][\s\S]*?display\s*:\s*none
hidden_divrB   rU   z(hidden HTML div (invisible instructions))zrm\s+-rf\s+/destructive_root_rmr;   destructivezrecursive delete from root)z+rm\s+(-[^\s]*)?r.*\$HOME|\brmdir\s+.*\$HOMEdestructive_home_rmr;   rb   z)recursive delete targeting home directory)zchmod\s+777insecure_permsmediumrb   zsets world-writable permissions)z	>\s*/etc/system_overwriter;   rb   z$overwrites system configuration file)z\bmkfs\bformat_filesystemr;   rb   zformats a filesystem)z\bdd\s+.*if=.*of=/dev/disk_overwriter;   rb   zraw disk write operation)zshutil\.rmtree\s*\(\s*[\"\'/]python_rmtreerB   rb   z/Python rmtree on absolute or root-relative path)ztruncate\s+-s\s*0\s+/truncate_systemr;   rb   z#truncates system file to zero bytes)z\bcrontab\bpersistence_cronre   persistencezmodifies cron jobs)zB\.(bashrc|zshrc|profile|bash_profile|bash_login|zprofile|zlogin)\bshell_rc_modre   rl   zreferences shell startup file)authorized_keysssh_backdoorr;   rl   zmodifies SSH authorized keys)z
ssh-keygen
ssh_keygenre   rl   zgenerates SSH keys)z-systemd.*\.service|systemctl\s+(enable|start)systemd_servicere   rl   z%references or enables systemd service)z/etc/init\.d/init_scriptre   rl   z references init.d startup script)z+launchctl\s+load|LaunchAgents|LaunchDaemonsmacos_launchdre   rl   z%macOS launch agent/daemon persistence)z/etc/sudoers|visudosudoers_modr;   rl   z'modifies sudoers (privilege escalation))zgit\s+config\s+--global\s+git_config_globalre   rl   z!modifies global git configuration)z#\bnc\s+-[lp]|ncat\s+-[lp]|\bsocat\breverse_shellr;   networkz potential reverse shell listener)z4\bngrok\b|\blocaltunnel\b|\bserveo\b|\bcloudflared\btunnel_servicerB   rw   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_portre   rw   zhardcoded IP address with port)z0\.0\.0\.0:\d+|INADDR_ANYbind_all_interfacesrB   rw   zbinds to all network interfaces)z /bin/(ba)?sh\s+-i\s+.*>/dev/tcp/bash_reverse_shellr;   rw   z+bash interactive reverse shell via /dev/tcp)z'python[23]?\s+-c\s+["\']import\s+socketpython_socket_onelinerr;   rw   z9Python one-liner socket connection (likely reverse shell))zsocket\.connect\s*\(\s*\(python_socket_connectrB   rw   z'Python socket connect to arbitrary host)z9webhook\.site|requestbin\.com|pipedream\.net|hookbin\.comexfil_servicerB   rw   z:references known data exfiltration/webhook testing service)z&pastebin\.com|hastebin\.com|ghostbin\.paste_servicere   rw   z0references paste service (possible data staging))zbase64\s+(-d|--decode)\s*\|base64_decode_piperB   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_stringre   r   z)hex-encoded string (possible obfuscation))z\beval\s*\(\s*["\']eval_stringrB   r   zeval() with string argument)z\bexec\s*\(\s*["\']exec_stringrB   r   zexec() with string argument)z1echo\s+[^\n]*\|\s*(bash|sh|python|perl|ruby|node)echo_pipe_execr;   r   z'echo piped to interpreter for execution)z?compile\s*\(\s*[^\)]+,\s*["\'].*["\']\s*,\s*["\']exec["\']\s*\)python_compile_execrB   r   zPython compile() with exec mode)zgetattr\s*\(\s*__builtins__python_getattr_builtinsrB   r   z5dynamic access to Python builtins (evasion technique))z#__import__\s*\(\s*["\']os["\']\s*\)python_import_osrB   r   zdynamic import of os module)zcodecs\.decode\s*\(\s*["\']python_codecs_decodere   r   z6codecs.decode (possible ROT13 or encoding obfuscation))zString\.fromCharCode|charCodeAtjs_char_codere   r   z=JavaScript character code construction (possible obfuscation))zatob\s*\(|btoa\s*\(	js_base64re   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_buildingrB   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_chainre   r   z/chain of unicode escapes (possible obfuscation))z.subprocess\.(run|call|Popen|check_output)\s*\(python_subprocessre   	executionzPython subprocess execution)zos\.system\s*\(python_os_systemrB   r   u)   os.system() — unguarded shell execution)zos\.popen\s*\(python_os_popenrB   r   u#   os.popen() — shell pipe execution)z%child_process\.(exec|spawn|fork)\s*\(node_child_processrB   r   zNode.js child_process execution)zRuntime\.getRuntime\(\)\.exec\(java_runtime_execrB   r   u'   Java Runtime.exec() — shell execution)z`[^`]*\$\([^)]+\)[^`]*`backtick_subshellre   r   z)backtick string with command substitution)z\.\./\.\./\.\.path_traversal_deeprB   	traversalz+deep relative path traversal (3+ levels up))z	\.\./\.\.path_traversalre   r   z&relative path traversal (2+ levels up))z/etc/passwd|/etc/shadowsystem_passwd_accessr;   r   z references system password files)z/proc/self|/proc/\d+/proc_accessrB   r   z3references /proc filesystem (process introspection))z	/dev/shm/dev_shmre   r   z.references shared memory (common staging area))z.xmrig|stratum\+tcp|monero|coinhive|cryptonightcrypto_miningr;   miningzcryptocurrency mining reference)zhashrate|nonce.*difficultymining_indicatorsre   r   z)possible cryptocurrency mining indicators)zcurl\s+[^\n]*\|\s*(ba)?shcurl_pipe_shellr;   supply_chainz*curl piped to shell (download-and-execute))z"wget\s+[^\n]*-O\s*-\s*\|\s*(ba)?shwget_pipe_shellr;   r   z*wget piped to shell (download-and-execute))zcurl\s+[^\n]*\|\s*pythoncurl_pipe_pythonr;   r   z curl piped to Python interpreter)z#\s*///\s*script.*dependenciespep723_inline_depsre   r   zAPEP 723 inline script metadata with dependencies (verify pinning))z pip\s+install\s+(?!-r\s)(?!.*==)unpinned_pip_installre   r   z#pip install without version pinning)znpm\s+install\s+(?!.*@\d)unpinned_npm_installre   r   z#npm install without version pinning)zuv\s+run\s+uv_runre   r   z/uv run (may auto-install unpinned dependencies))zD(curl|wget|httpx?\.get|requests\.get|fetch)\s*[\(]?\s*["\']https?://remote_fetchre   r   z"fetches remote resource at runtime)zgit\s+clone\s+	git_clonere   r   z"clones a git repository at runtime)zdocker\s+pull\s+docker_pullre   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_usagerB   r   z uses sudo (privilege escalation))zsetuid|setgid|cap_setuidsetuid_setgidr;   r   z.setuid/setgid (privilege escalation mechanism))NOPASSWDnopasswd_sudor;   r   z:NOPASSWD sudoers entry (passwordless privilege escalation))zchmod\s+[u+]?ssuid_bitr;   r   zsets SUID/SGID bit on a file)z0AGENTS\.md|CLAUDE\.md|\.cursorrules|\.clinerulesagent_config_modr;   rl   zTreferences agent config files (could persist malicious instructions across sessions))z'\.hermes/config\.yaml|\.hermes/SOUL\.mdhermes_config_modr;   rl   z.references Hermes configuration files directly)z \.claude/settings|\.codex/configother_agent_configrB   rl   z*references other agent configuration files)zI(?:api[_-]?key|token|secret|password)\s*[=:]\s*["\'][A-Za-z0-9+/=_-]{20,}hardcoded_secretr;   credential_exposurez,possible hardcoded API key, token, or secret)z(-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----embedded_private_keyr;   r   zembedded private key)z0ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{80,}github_token_leakedr;   r   z-GitHub personal access token in skill content)zsk-[A-Za-z0-9]{20,}openai_key_leakedr;   r   z(possible OpenAI API key in skill content)zsk-ant-[A-Za-z0-9_-]{90,}anthropic_key_leakedr;   r   z+possible Anthropic API key in skill content)zAKIA[0-9A-Z]{16}aws_access_key_leakedr;   r   z"AWS access key ID in skill content)z"\bDAN\s+mode\b|Do\s+Anything\s+Nowjailbreak_danr;   rU   z'DAN (Do Anything Now) jailbreak attempt)z"\bdeveloper\s+mode\b.*\benabled?\bjailbreak_dev_moder;   rU   z developer mode jailbreak attempt)z3hypothetical\s+scenario.*(?:ignore|bypass|override)hypothetical_bypassrB   rU   z1hypothetical scenario used to bypass restrictions)z$for\s+educational\s+purposes?\s+onlyeducational_pretextre   rU   z9educational pretext often used to justify harmful content)zW(respond|answer|reply)\s+without\s+(?:\w+\s+)*(restrictions|limitations|filters|safety)remove_filtersr;   rU   z1instructs agent to respond without safety filters)z>you\s+have\s+been\s+(?:\w+\s+)*(updated|upgraded|patched)\s+tofake_updaterB   rU   z3fake update/patch announcement (social engineering))zYnew\s+(?:\w+\s+)*policy|updated\s+(?:\w+\s+)*guidelines|revised\s+(?:\w+\s+)*instructionsfake_policyre   rU   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_exfilrB   r<   z4instructs agent to output/share conversation history)z6(send|post|upload|transmit)\s+.*\s+(to|at)\s+https?://send_to_urlrB   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.toml.yaml.pl.py.rb.sh.bash.json>   .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_unicoderB   rU   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,  7  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_skillrB  x  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*   c                 ,   t          j                    }|                                 rt          |                     d                    D ]}|                                ry|                    |                                           }|                    |	                    d          dz              |                    |
                                           n'|                    | 
                                           |                                S )z;Canonical SHA-256 over relative paths and exact file bytes.r0  r      )hashlibsha256r3  sortedr7  r8  r9  as_posixupdateencode
read_bytes	hexdigest)r-  hr   rA  s       r+   _content_digestrN    s    A *
 0 0 5 566 	1 	1I  "" 1++J77@@BBG,,w6777--//000		1 	
&&(()));;==r*   c                 &    dt          |            S )z8Full canonical digest used to bind scanner attestations.sha256:rN  r-  s    r+   full_content_hashrS    s    2_Z00222r*   findingc                        fddD             S )Nc                 2    i | ]}|t          |          S r)   )getattr).0keyrT  s     r+   
<dictcomp>z!_finding_dict.<locals>.<dictcomp>  s1       3C#&&   r*   r  r)   )rT  s   `r+   _finding_dictr[    s0        3    r*   )
source_url	cache_dirr\  r]  c                   t          |           }|p	| j        dz  }t          j        | d|                     d                                                    dd         }||                    dd          d          d| d	z  }	 t          j        |	                    d
                    }n# t          t          j        f$ r d}Y nw xY wt          |t                    r|                    d          |k    r|                    d          t          k    r|                    d          |k    r|                    d          |k    r~t!          | j        ||d         |d         d |                    dg           D             |d         |                    dd                    }	t          |          }
d|
d<   |
|	_        |	|
fS t'          | |          }	d |	j        D             }|||t          |	j        |	j        |t/          d |D                       |	j        |	j        dd}
	 |                    dd           |                    t          j        |
d           d!z   d
           n# t          $ r Y nw xY w|
|	_        |	|
fS )"zCReturn a scan plus attestation, caching only exact current content.z.scan-cache r  N   :r   -r   r  bundle_hashscanner_versionr/   r\  r0   r1   c                 &    g | ]}t          d i |S )r)   )r   rX  items     r+   
<listcomp>z%scan_skill_cached.<locals>.<listcomp>  s"    MMM$gooooMMMr*   r3   r5   r6   r4   r1  Ffresh)r/   c                 ,    g | ]}t          |          S r)   )r[  rf  s     r+   rh  z%scan_skill_cached.<locals>.<listcomp>  s     @@@d##@@@r*   c                     h | ]
}|d          S )r   r)   rf  s     r+   	<setcomp>z$scan_skill_cached.<locals>.<setcomp>  s    AAAl+AAAr*   T)r/   r\  rc  rd  r1   r0   r3   rulesr5   r6   ri  )parentsexist_okr   )indentr  )rS  parentrE  rF  rJ  rL  r  jsonloadsr  r  JSONDecodeError
isinstancer9   getSCANNER_VERSIONr-   r  r7   rB  r3   r1   r0   rG  r5   r6   mkdir
write_textdumps)r-  r/   r\  r]  rc  
cache_rootsource_identity
cache_filecachedresult
provenancer3   s               r+   scan_skill_cachedr    s    $J//K?j/-?Jn%>%>*%>%>%E%Eg%N%NOOYY[[\_]_\_`O!2!23!:!:1!=VVVVVVJJ00'0BBCCT)*   64   "

=))[88

,--@@

8$$..

<((J66!v}-vi7HMM&**Z2L2LMMMl+VZZ	25N5N	
 
 
 &\\
#
7!+z!!
6222F@@@@@H
;*v~)xAAAAABB'FNT J555djA>>>EPWXXXX   'F:s%   (B1 1CC,AI2 2
I?>I?Fr  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_POLICYrv  r0   VERDICT_INDEXr1   r  r3   )r  r  policyvidecisions        r+   should_allow_installr    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      )r;   rB   re   r   c                 :                         | j        d          S )N   )rv  r   )r@  severity_orders    r+   <lambda>z$format_scan_report.<locals>.<lambda>8  s    @R@RSTS]_`@a@a r*   )rY        ra     z   z "N<   "r4   TALLOWEDzNEEDS CONFIRMATIONBLOCKEDz
Decision:     — r  )r1   upperr  r.   r/   r0   r3   rG  r   ljustr   r   r    r!   r  join)r  r$  verdict_displaysorted_findingsr@  sevcatlocallowedreasonstatusr  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                 6    dt          |           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.
    rP  Nr`  rQ  rR  s    r+   content_hashr  N  s#     8_Z00"5777r*   	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+   r  z"_check_structure.<locals>.<lambda>q  s    e r*   r   r0  r   symlink_escaper;   r   zsymlink -> z*symlink points outside the skill directoryr  broken_symlinkre   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_skillrB   zKB totalz	skill is zKB total (limit: )r7  r8  
is_symlinkr&   r9  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)
r  r/  r3   
file_count
total_sizer@  rA  resolvedsizeexts
             r+   r6  r6  `  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  )rv  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  #rA  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 )Nr  FTc              3   B   K   | ]}t          j         |          V  d S N)fnmatch)rX  segps     r+   	<genexpr>z5_load_skill_ignore.<locals>.ignore.<locals>.<genexpr>A  s@       ( (03GOC++( ( ( ( ( (r*   )r   rH  r  _NEVER_IGNORABLE_ALWAYS_IGNORED_NAMES
startswithlstripendswithrstripfindr  any)rA  	rel_posixbasepatanchoredr3  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_FILENAMESr8  r  
splitlinesr  r  r  r  r  r&   bool)r  r  igrawr    r/  r  s         @r+   r4  r4  	  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   r  r   r   )r  r  TRUSTED_REPOS)r/   prefix_aliasesnormalized_sourceprefixr   s        r+   r2  r2  N  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 )r;   Nr   rX  r@  s     r+   r  z%_determine_verdict.<locals>.<genexpr>p  s)      BBAqzZ/BBBBBBr*   c              3   ,   K   | ]}|j         d k    V  dS )rB   Nr  r  s     r+   r  z%_determine_verdict.<locals>.<genexpr>q  s)      ::A1:'::::::r*   r   r   )r  )r3   has_criticalhas_highs      r+   r:  r:  k  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+   rl  z!_build_summary.<locals>.<setcomp>  s    ///!*///r*   z: r  z finding(s) in z, )r  r  rG  )r  r/   r  r1   r3   
categoriess         r+   r;  r;  {  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  )2__doc__r  r  rE  rr  dataclassesr   r   r   r   pathlibr   typingr   r	   rw  r  r  r  r   r-   r  r  r  r  r  r  r   r&   r,  rB  rN  rS  r9   r[  r  r  r  r  r  r6  r!  r  r  r  r  r4  r2  r:  r;  r)   r*   r+   <module>r     s   . 
			    ( ( ( ( ( ( ( ( ' ' ' ' ' ' ' '               $	 	 	 433
 2
 
 qq99         8 8 8 8 8 8 8 8d d dN              2> > > >d7m > > > >B8 84 8 8z 8 8 8 8v     3$ 33 3 3 3 3
7 t     / !/ / /// 	/
 d{/ :t/ / / /d) ) )D )U4QT9EU ) ) ) )X!z !c ! ! ! !H8T 8c 8 8 8 8$~ ~ ~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*   