
    j13                    x   d Z ddlmZ ddlZddlmc mZ ddl	Z	ddl
Z
ddlZddlZddlmZ ddlZdZdddZej$                  d        Zej(                  j+                  dg d	      d
        Zd Zd Zej(                  j+                  dg d      d        Zd Zd Zd Zd Zd Zd Zd Z d Z!d Z"d Z#d Z$d Z%y)u  task-2578 v3 audit-trail Bash capture regression tests.

회장 결정 2026-05-14 옵션 A: minimal patch post-tool-use.sh L137-160 IS_MEANINGFUL filter 제거 + v3 schema.

검증 항목:
- destructive command 박제 (rm / mv / truncate / `> file` / find -delete / git stash drop / git checkout -- file 등)
- redaction 적용 (api_keys / tokens / env_secrets / korean_pii)
- command_hash (SHA256 of raw)
- affected_path_candidates 추출
- claimed_actor / actor_verification_evidence / owner_attribution_status 기본값
- backward compat (v2 entries 무변경 — 본 test는 v3 박제만 검증)

본 test는 production hook을 stdin feed로 호출 + AUDIT_TRAIL_LOG_DIR override.
    )annotationsN)Pathz(/home/jay/.claude/hooks/post-tool-use.shc                h   d||d| id}i t         j                  dt        |      i}t        j                  dt
        gt        j                  |      dd|d      }|j                  }d	}||k(  }	|	st        j                  d
|	fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }
t        j                  d|j                          dz   d|
iz  }t#        t        j$                  |            dx}x}	}|dz  }|j'                         sg S |j)                         j+                         D cg c](  }|j-                         st        j.                  |      * c}S c c}w )uP   post-tool-use.sh를 stdin feed로 실행하고 audit-trail.jsonl entries 반환.Bashcommand	tool_namecwd
session_id
tool_inputAUDIT_TRAIL_LOG_DIRbashT
   inputcapture_outputtextenvtimeoutr   ==z2%(py2)s
{%(py2)s = %(py0)s.returncode
} == %(py5)sresultpy0py2py5zhook failed: z
>assert %(py7)spy7Nzaudit-trail.jsonl)osenvironstr
subprocessrun	HOOK_PATHjsondumps
returncode
@pytest_ar_call_reprcompare@py_builtinslocals_should_repr_global_name	_saferepr_format_assertmsgstderrAssertionError_format_explanationexists	read_text
splitlinesstriploads)r   log_dirr   r
   payloadr   r   @py_assert1@py_assert4@py_assert3@py_format6@py_format8log_filelines                 H/home/jay/workspace/tests/regression/test_audit_trail_bash_capture_v3.py	_run_hookrA      sA      '*	G >RZZ
=.G
=C^^	jj!F BB!BBBBBBBBB6BBB6BBBBBBBBB]6==/#BBBBBBBB,,H??	)1););)=)H)H)J[djjlDJJt[[[s   ?F/F/c                    | dz  S )Nlogs )tmp_paths    r@   tmp_audit_dirrF   6   s    f    zcommand, expected_pattern))zrm /tmp/test_file_abcrm)zrm -rf /tmp/some_dirrH   ),truncate -s 0 utils/replacement_pr_runner.pytruncate)zunlink /tmp/some_fileunlink)zfind . -name '*.tmp' -deletezfind-delete)zgit stash drop stash@{0}git-destructive)zgit reset --hard HEADrL   )z)git checkout origin/main -- utils/file.pyzgit-checkout-file)z0git worktree remove --force .worktrees/some-taskzgit-worktree-remove)z: > utils/some_file.pyz:>)z"cat /dev/null > utils/some_file.pyzcat-devnull-redirectc                |
   t        ||       }t        |      }d}||k(  }|st        j                  d|fd||f      dt	        j
                         v st        j                  t              rt        j                  t              nddt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }t        j                  dt        |             dz   d	|iz  }t        t        j                  |            d x}x}}|d
   }	|	d   }
d}|
|k(  }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t        t        j                  |            d x}
x}}|	d   }
d}|
|k(  }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t        t        j                  |            d x}
x}}|	d   }
d}|
|u }|st        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }t        j                  d|	       dz   d|iz  }t        t        j                  |            d x}
x}}|	d   }||v }|st        j                  d|fd||f      dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }t        j                  d| d|	d    d       d!z   d"|iz  }t        t        j                  |            d x}}|	d#   }
|
j                  }d$} ||      }|std%t        j                  |
      t        j                  |      t        j                  |      t        j                  |      d&z  }t        t        j                  |            d x}
x}x}}|	d'   }
d(}|
|k(  }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t        t        j                  |            d x}
x}}|	d)   }
d }|
|u }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t        t        j                  |            d x}
x}}y )*N   r   z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)slenentriesr   py1py3py6zexpected 1 entry, got z
>assert %(py8)spy8r   schema_version   z%(py1)s == %(py4)srS   py4assert %(py6)srU   toolr   is_destructiveTisz%(py1)s is %(py4)sz"is_destructive expected True, got z
>assert %(py6)sdestructive_pattern_matchedinz%(py0)s in %(py3)sexpected_patternr   rT   z	pattern 'z' not found in ''
>assert %(py5)sr   captured_bypost-tool-use.sh.v3Lassert %(py7)s
{%(py7)s = %(py3)s
{%(py3)s = %(py1)s.startswith
}(%(py5)s)
}rS   rT   r   r   owner_attribution_statusNOT_CLAIMEDclaimed_actor)rA   rP   r(   r)   r*   r+   r,   r-   r.   r0   r1   
startswith)rF   r   rf   rQ   @py_assert2@py_assert5r:   @py_format7@py_format9entry@py_assert0r;   @py_format5r9   @py_format4r<   @py_assert6r=   s                     r@   !test_destructive_pattern_capturedr{   @   s   " /Gw<E1E<1EEE<1EEEEEE3EEE3EEEEEEwEEEwEEE<EEE1EEE 6s7|nEEEEEEEEAJE!"'a'"a''''"a'''"'''a'''''''="F"=F""""=F"""="""F"""""""!"XdX"d*XXX"dXXX"XXXdXXX.PQVPW,XXXXXXXX$%BC CC  C               D    $%%5e<Y6Z5[[\]     A**A+@A*+@AAAAAAAA*AAA+@AAAAAAAAAA+,==,====,===,==========!)T)!T))))!T)))!)))T)))))))rG   c                D   t        d|       }t        |      }d}||k(  }|st        j                  d|fd||f      dt	        j
                         v st        j                  t              rt        j                  t              nddt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}x}}|d   }|d   }d}	||	k(  }|slt        j                  d|fd||	f      t        j                  |      t        j                  |	      dz  }
dd|
iz  }t        t        j                  |            d
x}x}}	|d   }d}	||	u }|slt        j                  d|fd||	f      t        j                  |      t        j                  |	      dz  }
dd|
iz  }t        t        j                  |            d
x}x}}	|d   }d}	||	k(  }|slt        j                  d|fd||	f      t        j                  |      t        j                  |	      dz  }
dd|
iz  }t        t        j                  |            d
x}x}}	y
)uU   일반 read-only command도 v3에서는 capture (IS_MEANINGFUL filter 제거 확인).zcat /tmp/test.txtrN   r   rO   rP   rQ   rR   assert %(py8)srV   Nr   rW   rX   rY   rZ   r\   rU   r^   Fr_   ra   rb    
rA   rP   r(   r)   r*   r+   r,   r-   r0   r1   )rF   rQ   rr   rs   r:   rt   ru   rv   rw   r;   rx   s              r@   2test_non_destructive_general_command_also_capturedr   _   s   +];Gw<1<1<133ww<1AJE!"'a'"a''''"a'''"'''a'''''''!"+e+"e++++"e+++"+++e+++++++./525/25555/2555/55525555555rG   c                @   t        d|       }t        |      }d}||k(  }|st        j                  d|fd||f      dt	        j
                         v st        j                  t              rt        j                  t              nddt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}x}}|d   d   }d}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }	dd|	iz  }t        t        j                  |            d
x}x}}y
)u?   git status / diff / log 등 read-only audit command도 capture.zgit status --shortrN   r   rO   rP   rQ   rR   r}   rV   Nr   r^   Fr_   ra   rZ   r\   rU   r   
rF   rQ   rr   rs   r:   rt   ru   rw   r;   rx   s
             r@   +test_git_status_captured_as_non_destructiver   i   s    ,m<Gw<1<1<133ww<11:&'050'50000'5000'00050000000rG   zcommand, placeholder_substring))z7echo TELEGRAM_BOT_TOKEN=abc123def456ghi789 | tee /tmp/x<REDACTED:env_secret>)z>echo GITHUB_TOKEN=ghp_abcdef1234567890ABCDEFGHIJKLMNOPQRSTUVWXr   )zexport AKIA1234567890ABCDEFz<REDACTED:api_key>)zFcurl -H 'Authorization: Bearer abc123def456ghi789jkl012' https://api.xz<REDACTED:token>)u"   echo '주민번호 991231-1234567'<REDACTED:korean_pii>)u   echo '연락처 010-1234-5678'r   )z'cat /home/jay/workspace/taskctl-bot.pemz<REDACTED:pem_path>)z&ssh -i /home/jay/.ssh/id_rsa user@host<REDACTED:ssh_key_path>)z(scp /home/jay/.ssh/id_ed25519 host:/tmp/r   )zJecho -----BEGIN RSA PRIVATE KEY-----abc123def-----END RSA PRIVATE KEY-----<REDACTED:pem_block>)zBecho -----BEGIN CERTIFICATE-----xyz789ghi-----END CERTIFICATE-----r   c                   t        ||       }t        |      }d}||k(  }|st        j                  d|fd||f      dt	        j
                         v st        j                  t              rt        j                  t              nddt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            d x}x}}|d	   }	|	d
   }
d}|
|u }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t        t        j                  |            d x}
x}}|	d   }||v }|st        j                  d|fd||f      dt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }t        j                  d|	d          dz   d|iz  }t        t        j                  |            d x}}y )NrN   r   rO   rP   rQ   rR   r}   rV   r   redaction_appliedTr_   ra   rZ   r\   rU   r   rc   re   placeholder_substringrg   zplaceholder missing: ri   r   )rA   rP   r(   r)   r*   r+   r,   r-   r0   r1   r.   )rF   r   r   rQ   rr   rs   r:   rt   ru   rv   rw   r;   rx   r9   ry   r<   s                   r@   test_redaction_appliedr   u   st   0 /Gw<1<1<133ww<1AJE$%--%----%---%----------$))$4` $44``` $4`````` ``` ```$4```8MeT]N^M_6```````rG   c                @   t        d|       }t        |      }d}||k(  }|st        j                  d|fd||f      dt	        j
                         v st        j                  t              rt        j                  t              nddt	        j
                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }dd	|iz  }t        t        j                  |            d x}x}}|d
   d   }d}||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }	dd|	iz  }t        t        j                  |            d x}x}}y )Nzgit commit -m 'test commit'rN   r   rO   rP   rQ   rR   r}   rV   r   r   Fr_   ra   rZ   r\   rU   r   r   s
             r@   -test_redaction_not_applied_for_normal_commandr      s    5}EGw<1<1<133ww<11:)*3e3*e3333*e333*333e3333333rG   c                   d}t        j                  |j                               j                         }t	        ||       }t        |      }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  t
              rt        j                  t
              nddt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }dd	|iz  }t        t        j                  |            d
x}x}}|d   }	|	d   }
|
|k(  }|st        j                  d|fd|
|f      t        j                  |
      dt        j                         v st        j                  |      rt        j                  |      nddz  }dd|iz  }t        t        j                  |            d
x}
}d}
|	d   }|
|v }|slt        j                  d|fd|
|f      t        j                  |
      t        j                  |      dz  }dd|iz  }t        t        j                  |            d
x}
x}}y
)uD   command_hash는 raw command (redaction 전) 의 SHA256이어야 함.z*echo TELEGRAM_BOT_TOKEN=abc123def456ghi789rN   r   rO   rP   rQ   rR   r}   rV   Nr   command_hash)z%(py1)s == %(py3)sexpected_hashrS   rT   assert %(py5)sr   z	<REDACTEDr   rc   z%(py1)s in %(py4)srZ   r\   rU   )hashlibsha256encode	hexdigestrA   rP   r(   r)   r*   r+   r,   r-   r0   r1   )rF   raw_commandr   rQ   rr   rs   r:   rt   ru   rv   rw   ry   r<   r;   rx   s                  r@   *test_command_hash_is_sha256_of_raw_commandr      s{   >KNN;#5#5#78BBDM]3Gw<1<1<133ww<1AJE 1 M1111 M111 111111M111M1111111*%	**;*****;****;***********rG   c                h   d}t        ||       }| j                  dz  }t        ||      }|d   d   }|d   d   }||k(  }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd	|iz  }	t        t        j                  |	            d
x}x}}y
)u   동일 command는 동일 hash.zrm /tmp/same_filelogs2r   r   r   rY   rZ   r\   rU   N)rA   parentr(   r)   r-   r0   r1   )
rF   cmdentries1tmp2entries2rw   r;   rr   rx   rt   s
             r@   test_command_hash_deterministicr      s    
Cm,H')Dd#HA;~&E(1+n*EE&*EEEEE&*EEEE&EEE*EEEEEEEErG   c                    t        d|       }d}|d   d   }||v }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd	|iz  }t	        t        j
                  |            d x}x}}y )
NrI   zutils/replacement_pr_runner.pyr   affected_path_candidatesrc   r   rZ   r\   rU   rA   r(   r)   r-   r0   r1   rF   rQ   rw   r;   rr   rx   rt   s          r@   -test_affected_path_extracted_from_destructiver      si    FVG+Uwqz:T/UU+/UUUUU+/UUUU+UUU/UUUUUUUUrG   c                   d}t        ||       }|d   d   }d |D        }t        |      }|sddt        j                         v st	        j
                  t              rt	        j                  t              ndt	        j                  |      t	        j                  |      dz  }t        t	        j                  |            d x}}d |D        }t        |      }|sddt        j                         v st	        j
                  t              rt	        j                  t              ndt	        j                  |      t	        j                  |      dz  }t        t	        j                  |            d x}}y )	Nz<git checkout origin/main -- utils/a.py tests/regression/b.pyr   r   c              3  $   K   | ]  }d |v  
 yw)za.pyNrD   .0ps     r@   	<genexpr>z.test_affected_path_multiple.<locals>.<genexpr>        *qv{*   z,assert %(py4)s
{%(py4)s = %(py0)s(%(py2)s)
}any)r   r   r[   c              3  $   K   | ]  }d |v  
 yw)zb.pyNrD   r   s     r@   r   z.test_affected_path_multiple.<locals>.<genexpr>   r   r   )	rA   r   r*   r+   r(   r,   r-   r0   r1   )rF   r   rQ   pathsr9   r;   rx   s          r@   test_affected_path_multipler      s    
HC]+GAJ12E*E**3*********3***3***************E**3*********3***3**************rG   c                "   t        d|       }|d   d   }g }||k(  }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t	        t        j
                  |            d x}x}}y )	Nlsr   r   r   rY   rZ   r\   rU   r   r   s          r@   *test_affected_path_empty_for_no_file_tokenr      se    m,G1:017R71R77771R7771777R7777777rG   c                "   t        d|       }|d   }|d   }d }||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t	        t        j
                  |            d x}x}}|d	   }d }||u }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t	        t        j
                  |            d x}x}}|d
   }d}||k(  }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t	        t        j
                  |            d x}x}}y )Nz
echo hellor   rp   r_   ra   rZ   r\   rU   actor_verification_evidencern   ro   r   rY   r   )rF   rQ   rv   rw   r;   rr   rx   rt   s           r@   test_attribution_defaultsr      s   m4GAJE!)T)!T))))!T)))!)))T)))))))./747/47777/4777/77747777777+,==,====,===,==========rG   c                ^   t        d|       }|d   }h d}||j                         z
  }| }|s~t        j                  d|       dz   ddt	        j
                         v st        j                  |      rt        j                  |      ndiz  }t        t        j                  |            d }|d   }d	}||k(  }	|	slt        j                  d
|	fd||f      t        j                  |      t        j                  |      dz  }
dd|
iz  }t        t        j                  |            d x}x}	}y )Nzecho schema-testr   >   tsbotr
   filer]   memberts_kstr   sessiontask_idrj   r   rp   r^   rW   r   r   rn   r   rb   zmissing v3 fields: z
>assert not %(py0)sr   missingrW   rX   r   rY   rZ   r\   rU   )rA   keysr(   r.   r*   r+   r,   r-   r0   r1   r)   )rF   rQ   rv   requiredr   r9   @py_format2rw   r;   rr   rx   rt   s               r@   &test_schema_v3_required_fields_presentr      s    *M:GAJEH, %G;7;77-gY7777777w777w777777!"'a'"a''''"a'''"'''a'''''''rG   c                T   t        d|       }|d   d   }|j                  }d} ||      }|stdt        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }t	        t        j
                  |            d x}x}x}}y )Nzecho markerr   rj   rk   rl   rm   )rA   rq   r(   r-   r0   r1   )rF   rQ   rw   rr   r:   rz   r=   s          r@   test_captured_by_markerr      s}    }5G1:m$F$//F0EF/0EFFFFF$FFF/FFF0EFFFFFFFFFFrG   c                   t        d|       }|d   }|d   }d}||k(  }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd	|iz  }t	        t        j
                  |            d
x}x}}d}||v }|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }dd|iz  }	t	        t        j
                  |	            d
x}}d}||v }|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }dd|iz  }	t	        t        j
                  |	            d
x}}d}||v }|st        j                  d|fd||f      t        j                  |      dt        j                         v st        j                  |      rt        j                  |      nddz  }dd|iz  }	t	        t        j
                  |	            d
x}}y
)uW   v2 reader가 사용하던 'tool', 'bot', 'task_id', 'command' field가 v3에도 존재.z
git statusr   r]   r   r   rY   rZ   r\   rU   Nr   rc   )z%(py1)s in %(py3)srv   r   r   r   r   r   )	rA   r(   r)   r-   r0   r1   r*   r+   r,   )
rF   rQ   rv   rw   r;   rr   rx   rt   ry   r<   s
             r@   &test_v3_entry_has_legacy_compat_fieldsr     so   m4GAJE="F"=F""""=F"""="""F"""""""5E>5E5EE999999rG   c                L   dddddid}i t         j                  dt        |       i}t        j                  dt
        gt        j                  |      d	d	|d
      }|j                  }d}||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                   |            dx}x}}y)u(   빈 command 입력에도 hook은 exit 0.r   z/tmpsr   r~   r   r   r   Tr   r   r   r   r   r   r   assert %(py7)sr   N)r   r    r!   r"   r#   r$   r%   r&   r'   r(   r)   r*   r+   r,   r-   r0   r1   )	rF   r8   r   r   r9   r:   r;   r<   r=   s	            r@   +test_hook_exit_code_zero_with_empty_commandr     s    "6U^`bTcdG
CRZZ
C.M0B
CC^^	jj!F !!!!!!!!!!!!6!!!6!!!!!!!!!!!!!rG   c                   i t         j                  dt        |       i}t        j                  dt
        gddd|d      }|j                  }d}||k(  }|st        j                  d|fd	||f      d
t        j                         v st        j                  |      rt        j                  |      nd
t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            d x}x}}y )Nr   r   znot-json-at-allTr   r   r   r   r   r   r   r   r   )r   r    r!   r"   r#   r$   r'   r(   r)   r*   r+   r,   r-   r0   r1   )rF   r   r   r9   r:   r;   r<   r=   s           r@   ,test_hook_exit_code_zero_with_malformed_jsonr   %  s    
CRZZ
C.M0B
CC^^	F !!!!!!!!!!!!6!!!6!!!!!!!!!!!!!rG   )z	test-sessz/home/jay/workspace)
r   r!   r7   r   r   r!   r
   r!   returnz
list[dict])&__doc__
__future__r   builtinsr*   _pytest.assertion.rewrite	assertionrewriter(   r   r%   r   r"   pathlibr   pytestr$   rA   fixturerF   markparametrizer{   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   rD   rG   r@   <module>r      s    #     	   6	\2    *! *61 $.a/.a4
+FV
+8>(<G"
"rG   