
    `gj                     f   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d?dededee   fd Zd@d!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"	 d@ddd(d!ed"ed)ed*edz  deee!f   f
d+Z#dAd,ed-e$dee$ef   fd.Z%d,edefd/Z&d!edefd0Z'dBd1edee   fd2Z(d3edefd4Z)d5Z* e+e*      Z,d6hZ-d1efd7Z.d"edefd8Z/d9ee   defd:Z0d;ed"ed<ed=ed9ee   defd>Z1y)Cu  
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                   T    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<   y)	Finding
pattern_idseveritycategoryfilelinematchdescriptionN)__name__
__module____qualname__str__annotations__int     E/root/.hermes/venv/lib/python3.12/site-packages/tools/skills_guard.pyr   r   J   s&    OMM
I
IJr*   r   c                       e Zd ZU eed<   eed<   eed<   eed<    ee      Zee	   ed<   dZ
eed<   dZeed	<    ee      Zeed
<   y)
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   sM    OKL#D9Hd7m9JGS!$7OT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                  j                         t        vr| j                   dk7  rg S 	 | j	                  d      }g }|j                  d      }t               }t        D ]  \  }}}}	}
t        |d      D ]  \  }}||f|v rt        j                  ||t        j                        s3|j                  ||f       |j                         }t!        |      dkD  r|dd	 d
z   }|j#                  t%        |||	||||
               t        |d      D ]W  \  }}t&        D ]I  }||v st)        |      }|j#                  t%        ddd||dt+        |      dd| dd| d              W Y |S # t
        t        f$ r g cY S w xY w)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  s    >>';;	R\@\	%%w%7 HMM$E5D :I 5h+ a0 	GAtQx4yy$6#q"#zz||$s*#/#5#=L"%%!& +! 	( U!, 4# 	Dt|.t4	2#(!s4yoR	{!<">ykIj k!  	 OW ( 	s   F FF
skill_pathr/   c           
         | j                   }t        |      }g }| j                         rt        |       }|j	                  t        | |             | j                  d      D ]Q  }|j                         st        |j                  |             } ||      r7|j	                  t        ||             S n5| j                         r%|j	                  t        | | j                                t        |      }t        |||||      }	t        |||||t        j                  t         j"                        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#J/ 	,ZGH !!#& 	7Ayy{!--
34#;##Ia$56	7 
			Ij*//BC .GZg|TG<<-779 r*   c                    t        j                         }| j                         rt        | j	                  d            D ]t  }|j                         s|j                  |       j                         }|j                  |j                  d      dz          |j                  |j                                v |j                         S |j                  | j                                |j                         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6 	1I  "++J7@@BG,w67--/0		1 ;;= 	
&&();;=r*   c                     dt        |        S )z8Full canonical digest used to bind scanner attestations.sha256:rN  r-  s    r+   full_content_hashrS    s    _Z0122r*   findingc                 B    dD ci c]  }|t        | |       c}S c c}w )Nr  )getattr)rT  keys     r+   _finding_dictrX    s,    3 3C#&&   s   )
source_url	cache_dirrY  rZ  c                
   t        |       }|xs | j                  dz  }t        j                  | d| j	                  d            j                         dd }||j                  dd      d    d| d	z  }	 t        j                  |j                  d
            }t        |t              r|j                  d      |k(  r|j                  d      t        k(  r|j                  d      |k(  r|j                  d      |k(  rwt!        | j"                  ||d   |d   |j                  dg       D 	cg c]  }	t%        d i |	 c}	|d   |j                  dd            }
t        |      }d|d<   ||
_        |
|fS t)        | |      }
|
j*                  D 	cg c]  }	t-        |	       }}	|||t        |
j.                  |
j0                  |t3        |D 	ch c]  }	|	d   	 c}	      |
j4                  |
j6                  dd}	 |j9                  dd       |j;                  t        j<                  |d      dz   d
       ||
_        |
|fS # t        t        j                  f$ r d}Y w xY wc c}	w c c}	w c c}	w # t        $ r Y Gw xY w)!zCReturn a scan plus attestation, caching only exact current content.z.scan-cache r  N   :r   -r   r  bundle_hashscanner_versionr/   rY  r0   r1   r3   r5   r6   r4   r1  Ffresh)r/   r   T)r/   rY  r`  ra  r1   r0   r3   rulesr5   r6   rb  )parentsexist_okr   )indentr  r)   )rS  parentrE  rF  rJ  rL  r  jsonloadsr  r  JSONDecodeError
isinstancer9   getSCANNER_VERSIONr-   r  r   r7   rB  r3   rX  r1   r0   rG  r5   r6   mkdir
write_textdumps)r-  r/   rY  rZ  r`  
cache_rootsource_identity
cache_filecacheditemresult
provenancer3   s                r+   scan_skill_cachedrx    s    $J/K?j//-?Jnnxr*%>%E%Eg%NOYY[\_]_`O!2!23!:1!= >a?PPUVVJJ00'0BC 	64 

=)[8

,-@

8$.

<(J6!v}-vi7H28**Z2LM$gooMl+VZZ	25N	
 &\
#
7!+z!!
62F06@d#@H@
;*v~~))xAl+AB''FNNTJ5djjA>EPWX (F:C T))*  N A
 B  s6   8%I .I'I,I1<>I6 I$#I$6	JJrv  forcec           	         t         j                  | j                  t         d         }t        j                  | j                  d      }||   }|dk(  rdd| j                   d| j                   dfS |rE| j                  dk(  r| j                  d	v s(dd
| j                   dt        | j                         dfS |dk(  r5dd| j                   d| j                   dt        | j                         dfS | j                  dk(  r6| 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_POLICYrl  r0   VERDICT_INDEXr1   r  r3   )rv  ry  policyvidecisions        r+   should_allow_installr    s     2 2N;4OPF			6>>1	-BbzH7y!3!3 4Ifnn=MYWWWfnn38J8JNf8f&v~~&6 7FOO$%Z1
 	

 5%f&8&8%9FNNCSS]6??#$J0
 	
 ~~$););?W)W**++H6??#$$^`
 	
 
F&&'z&..1Av
  D	F r*   c                    g }| j                   j                         }|j                  d| j                   d| j                   d| j
                   d|        | j                  rddddd	t        | j                  fd
      }|D ]  }|j                  j                         j                  d      }|j                  j                  d      }|j                   d|j                   j                  d      }|j                  d| d| d| d|j                  dd  d	        |j                  d       t        |       \  }}	|du rd}
n|d}
nd}
|j                  d|
 d|	        dj                  |      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                  | j                  d      S )N   )rl  r   )r@  severity_orders    r+   <lambda>z$format_scan_report.<locals>.<lambda>8  s    @R@RSTS]S]_`@a r*   )rW        r^     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)rv  r$  verdict_displaysorted_findingsr@  sevcatlocallowedreasonstatusr  s              @r+   format_scan_reportr  *  sq    Enn**,O	LL6&++,Bv}}oQv?Q?Q>RR^_n^opq&'aJ 6ab  	DA**""$**1-C**""2&CVVHAaffX&,,R0CLL2cU!C5#c!''#2,rBC		D 	R*62OGV$	%	LL:fXU6(3499Ur*   c                 $    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     _Z0"5677r*   	skill_dirc                    |d }g }d}d}| j                  d      D ]  }|j                         s|j                         s%t        |j	                  |             } ||      rH|dz  }|j                         rU	 |j                         }|j                  | j                               s$|j                  t        ddd|dd| d	
             	 |j                         j                  }||z  }|t        dz  kD  r5|j                  t        ddd|d|dz   dd|dz   dt         d
             |j                  j                         }	|	t        v r(|j                  t        ddd|dd|	 d|	 d
             |	dvsc|j                         j                   dz  s|j                  t        ddd|ddd
              |t"        kD  r/|j                  t        dddd d| d!d"| d#t"         d$
             |t$        dz  kD  r5|j                  t        d%d&dd d|dz   d'd(|dz   d)t$         d
             |S # t        $ r% |j                  t        ddd|ddd
             Y w xY w# t        $ r Y ^w xY w)*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.
    c                      y)NFr)   )_rels    r+   r  z"_check_structure.<locals>.<lambda>q  s    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yy{1<<>!--	*+#;a
 <<>99;..y/@/@/BCOOG#3!+!,  +H:6$P% & 	668##D$J
 $t++OOG+!%b)&tt|nK@R?SSVW  hhnn..OOG(#% &6se;UV  ;;@P@PSX@XOOG2!%*` KM` N"'!L'$ZL?OqQ
 	 %,,(!4'(1#J$$6#77HIZH[[^_
 	 Oa  	/%(* <! 	   		s%   ;AH>I/>*I,+I,/	I<;I<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"}|j                  | 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  )rl  r"  )r*  namess     r+   r!  r!    s    $) 	% 	-	
 	# 	' 	" 	1 	/ 	/ 	# 	. 	. 	- 	-  	(!" 	+#E& 99TRD	#/00r*   )z.skillignorez.clawhubignorer   c                 P   g t         D ]r  }| |z  }	 |j                         rZ|j                  d      j                         D ]7  }|j	                         }|r|j                  d      r'j                  |       9 t dt        dt        ffd}|S # t        t        f$ r Y w xY w)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        |       j                         }|j                  d      d   }|t        v ry|t        v ryD ]  }|j                  d      }|j                  d      j                  d      }j                  d      sJ|r@|k(  s|j                  dz         r y|s"d|z   dz   j                  dz   dz         dk7  r yt        j                  |      r y|rt        j                  |      r ydvr%t        fd|j                  d      D              r y|j                  dz         s y y)Nr  FTc              3   J   K   | ]  }t        j                   |        y wN)fnmatch).0segps     r+   	<genexpr>z5_load_skill_ignore.<locals>.ignore.<locals>.<genexpr>A  s!      (03GOOC+(s    #)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!  sA   I&&(	s#B'##(( 	 C~~c*H

3AZZ_FA>Y%9%9!c'%BS9_s%:$@$@q3$OSU$U y!,??4+a<C (7@s7K( %   ''C0=	 > r*   )_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DDF *C99;D4??3#7 OOD)	*	
(C (D (T M[ #G, 		s   A*BB%$B%c                     d}| }|D ]#  }|j                  |      s|t        |      d } n |dk(  ry|dk(  ryt        D ]  }||k(  s|j                  | d      s y y)	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+, ? O+ J& ! '+<+G+G7)ST+V r*   r3   c                 ^    | syt        d | D              }t        d | D              }|ry|ryy)z6Determine the overall verdict from a list of findings.r   c              3   :   K   | ]  }|j                   d k(    yw)r;   Nr   r  r@  s     r+   r  z%_determine_verdict.<locals>.<genexpr>p  s     BAqzzZ/B   c              3   :   K   | ]  }|j                   d k(    yw)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  s7    BBBL:::Hr*   r  trustr1   c                     |s|  dS |D ch c]  }|j                    }}|  d| dt        |       ddj                  t        |             S c c}w )z,Build a one-line summary of the scan result.z!: clean scan, no threats detectedz: r  z finding(s) in z, )r   r  r  rG  )r  r/   r  r1   r3   r@  
categoriess          r+   r;  r;  {  s`    899&./!**/J/V2gYeCM?/$))FS]L^B_A`aa 0s   A)r4   )r   )Fr  )2__doc__r  r  rE  rh  dataclassesr   r   r   r   pathlibr   typingr   r	   rm  r  r{  r|  r   r-   r  r  r  r  r  r  r   r&   r,  rB  rN  rS  r9   rX  rx  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    8 8 8dN      2> > >d7m >B84 8 8z 8v  3$ 33 3
7 t  / !/// 	/
 d{/ :t/d) )D )U4QT9EU )X!z !c !H8T 8c 8$~ ~d7m ~B1S 1S 1@ =  34 < B$ BJ  :g 3  b bc b# b btT[} bad br*   