
    wjC1                       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 ee      j#                         j$                  d   Z ee      ej*                  vr"ej*                  j-                  d ee             ddlmZmZmZmZmZmZmZmZ ddl m!Z! dZ"d	Z#d
dd dZ$e#dd!dZ%d Z&d Z'd Z(d Z)d Z*d Z+d Z,d Z-d Z.d Z/d Z0d Z1d Z2d Z3d Z4d Z5d Z6y)"u  anu_v2.tests.test_owner_trigger_security_boundaries_2554 — 보안 경계 회귀 (task-2554).

system spec §10 #10 + 회장 §8 추가 5건 (#15~#19) 1:1 박제:
  #10 token value가 log에 출력되면 fail
  #15 single allowed endpoint 검증 (POST issues/{n}/comments only)
  #16 forbidden API list hard-block 정적/동적 검증
  #17 OWNER_GEMINI_TRIGGER_TOKEN 외 token 사용 시 fail
  #18 GH_TOKEN fallback fail
  #19 BOT_GITHUB_TOKEN 사용 시 fail
    )annotationsN)Path   )FORBIDDEN_ENDPOINT_PATTERNSFORBIDDEN_TOKEN_ENV_NAMESForbiddenEndpointErrorOwnerTriggerOnlyTOKEN_ENV_NAMETokenBoundaryViolationassert_endpoint_allowedassert_token_boundary)OwnerTriggerAudit(aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa4ghp_REAL_OWNER_TOKEN_VALUE_MUST_NEVER_BE_LOGGED_zzzzg   )prc                   dd|t         dddddddd}| d	z  }|j                  t        j                  |      d
       |S )Nz anu_v2.owner_trigger_decision.v1z	task-2554TFr   "POST_GEMINI_REVIEW_TRIGGER_COMMENTz/gemini review)schematask_idr   current_head
queue_headcurrent_head_confirmedgemini_evidence_freshnudge_count_for_pr_headallowed_actioncomment_bodyallowedzdecision.jsonutf-8encoding)_HEAD_A
write_textjsondumps)tmp_pathr   decisionps       O/home/jay/workspace/anu_v2/tests/test_owner_trigger_security_boundaries_2554.py_write_decisionr*   +   sT    4"&!&#$>(H 	?"ALLH%L8H    tokenc               `    g dfd}dfd}t        |       }t        | |||      }||fS )Nc                8    j                  | |||d       ddiS )N)methodpathbodyheadersstatus   )append)r0   r1   r2   r3   postss       r)   	http_postz _build_module.<locals>.http_postA   s"    dwWX#r+   c                      S )N r,   s   r)   token_providerz%_build_module.<locals>.token_providerE   s    r+   )workspace_rootr8   r;   audit)
r0   strr1   r>   r2   dictr3   r?   returnr?   )r@   r>   )r   r	   )r&   r-   r8   r;   r=   modr7   s    `    @r)   _build_modulerB   >   sC    E h'E
%	C ur+   c                :   t        |       }t        |       \  }}}|j                  |ddt               |j                  j                  d      }t        |v}|st        j                  d|fdt        |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	d
z  }dd|iz  }t        t        j                  |            d }dD ]  }	|	|v}|st        j                  d|fd|	|f      dt        j                         v st        j                  |	      rt        j                  |	      ndd	t        j                         v st        j                  |      rt        j                  |      nd	d
z  }t        j                  d|	      dz   d|iz  }t        t        j                  |            d } y )Nordecision_pathownerrepocurrent_head_actualr   r    not inz%(py0)s not in %(py2)s_SECRET_SENTINELrawpy0py2zassert %(py4)spy4)ghp_github_pat_zBearer ghu_ghs_ghr_sentzaudit contains token sentinel 
>assert %(py4)s)r*   rB   trigger_gemini_reviewr"   r1   	read_textrN   
@pytest_ar_call_reprcompare@py_builtinslocals_should_repr_global_name	_safereprAssertionError_format_explanation_format_assertmsg)
r&   rG   rA   _r=   rO   @py_assert1@py_format3@py_format5rY   s
             r)   'test_token_raw_value_not_in_audit_jsonlrj   T   sJ   #H-M!(+MCE##	   **



0C3&&&&3&&&&&&&&&&&&&&&3&&&3&&&&&&&J J3IIIt3IIIIIItIIItIIIIII3III3IIII"@ IIIIIIIJr+   c           
        t        |       }t        |       \  }}}|j                  t        j                         |j                  |ddt               |j                         }|j                  }t        |v}|st        j                  d|fdt        |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                  |      dz  }	d	d
|	iz  }
t!        t        j"                  |
            d x}}|j$                  }t        |v}|st        j                  d|fdt        |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                  |      dz  }	d	d
|	iz  }
t!        t        j"                  |
            d x}}|j&                  D ]  }|j(                  } |       }t+        |      }t        |v}|sWt        j                  d|fdt        |f      dt        j                         v st        j                  t              rt        j                  t              nd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                  |      t        j                  |      dz  }dd|iz  }t!        t        j"                  |            d x}x}x}} y )NrD   rE   rF   rK   )z/%(py0)s not in %(py4)s
{%(py4)s = %(py2)s.out
}rN   captured)rQ   rR   rS   assert %(py6)spy6)z/%(py0)s not in %(py4)s
{%(py4)s = %(py2)s.err
})zk%(py0)s not in %(py9)s
{%(py9)s = %(py2)s(%(py7)s
{%(py7)s = %(py5)s
{%(py5)s = %(py3)s.getMessage
}()
})
}r>   record)rQ   rR   py3py5py7py9assert %(py11)spy11)r*   rB   	set_levelloggingDEBUGr[   r"   
readouterroutrN   r]   r^   r_   r`   ra   rb   rc   rd   errrecords
getMessager>   )r&   capsyscaplogrG   rA   rf   rl   @py_assert3rg   ri   @py_format7ro   @py_assert4@py_assert6@py_assert8@py_format10@py_format12s                    r)   ,test_token_raw_value_not_in_capsys_or_caplogr   f   sB   #H-Mh'ICA
W]]###	     "H#+<</<////<///////////////8///8///<///////#+<</<////<///////////////8///8///<///////.. @+1+<+<?+<+>?s+>'??'?????'????????????????s???s??????6???6???+<???+>???'????????@r+   c                    t         dz  dz  j                  d      } 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)ul   모듈 source 정적 검사: 'ghp_' / 'github_pat_' / 'Bearer ' 패턴이 실제 token처럼 하드코딩 X.anu_v2owner_trigger_only.pyr   r    rT   rK   z%(py1)s not in %(py3)ssrcpy1rp   assert %(py5)srq   NrU   
WORKSPACE_ROOTr\   r]   r^   rb   r_   r`   ra   rc   rd   r   @py_assert0@py_assert2@py_format4@py_format6s        r)   /test_module_source_has_no_hardcoded_token_valuer   {   s    H$'>>
I
ISZ
I
[C 666#=####=###=################r+   c                    ddl m}  | j                  }d} ||      }|sddt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      t        j                  |      t        j                  |      dz  }t        t        j                  |            dx}x}}dD ]	  }| j                  } ||      }| }|st        j                  d	|       d
z   dt        j                         v st        j                  |       rt        j                  |       ndt        j                  |      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      dz  }t        t        j                  |            dx}x}} y)uG   static check: _ALLOWED_PATH_RE 은 POST issues/{n}/comments 만 매치.r   )_ALLOWED_PATH_RE/repos/o/r/issues/103/commentszGassert %(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.match
}(%(py4)s)
}r   )rQ   rR   rS   rn   N)/repos/o/r/issues/103/repos/o/r/issues/103/labelsz/repos/o/r/pulls/103/comments/repos/o/r/pulls/103/reviews/repos/o/r/pulls/103/mergez/repos/o/r/issues/commentsz /repos/o/r/issues/103/comments/1zpath should NOT match: zM
>assert not %(py5)s
{%(py5)s = %(py2)s
{%(py2)s = %(py0)s.match
}(%(py3)s)
}r1   )rQ   rR   rp   rq   )anu_v2.owner_trigger_onlyr   matchr_   r`   r]   ra   rb   rc   rd   re   )r   rg   r   @py_assert5r   r1   r   r   s           r)   0test_only_one_allowed_endpoint_path_regex_staticr      s.   : !!C"BC!"BCCCCCCCCCCCCCC!CCC"BCCCCCCCCCC 	R $))Q)$/Q//Q/QQ3J4&1QQQQQQQ#QQQ#QQQ)QQQQQQ$QQQ$QQQ/QQQQQQ	Rr+   c                b   t        |       }t        |       \  }}}|j                  |dd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}||k(  }|slt        j                  d|fd||f      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            d x}x}}t        j                  }d}|
d   } |||      }|sddt        j                         v st        j                  t              rt        j                  t              ndt        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }	t        t        j                  |	            d x}x}x}}y )NrD   rE   rF      ==)z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} == %(py6)slenr7   rQ   r   rp   rn   assert %(py8)spy8r   r0   POST)z%(py1)s == %(py4)s)r   rS   rm   rn   z(^/repos/[^/]+/[^/]+/issues/\d+/comments$r1   zPassert %(py8)s
{%(py8)s = %(py2)s
{%(py2)s = %(py0)s.match
}(%(py4)s, %(py6)s)
}re)rQ   rR   rS   rn   r   )r*   rB   r[   r"   r   r]   r^   r_   r`   ra   rb   rc   rd   r   r   )r&   rG   rA   r7   rf   r   r   r   r   @py_format9callr   r   ri   rg   @py_assert7s                   r)   1test_single_endpoint_runtime_one_call_per_triggerr      s   #H-M!(+MC##	   u::?:33uu:8D>#V#>V####>V###>###V#######88N?NfN8?NNNNNNNN2NNN2NNN8NNN?NNNNNNNNNNNNNr+   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                  t              rt        j                  t              ndt        j                  |       t        j                  |      dz  }dd|iz  }t        t        j                  |            d	x} x}}y	)
u,   11 패턴 모두 정의됨을 정적 검증.
   )>=)z0%(py3)s
{%(py3)s = %(py0)s(%(py1)s)
} >= %(py6)sr   r   r   r   r   N)
r   r   r]   r^   r_   r`   ra   rb   rc   rd   )r   r   r   r   r   s        r)   7test_forbidden_11_endpoints_static_pattern_completenessr      s     *+1r1+r1111+r11111131113111111*111*111+111r1111111r+   c                     g d} | D ]3  \  }}t        j                  t              5  t        ||       ddd       5 y# 1 sw Y   @xY w)uL   동적 호출 시 11 endpoint 케이스 모두 ForbiddenEndpointError raise.))r   r   )r   r   )PATCHz/repos/o/r/git/refs/heads/main)r   z/repos/o/r/git/refs)PUTz/repos/o/r/contents/file.py)r   r   )r   r   )DELETEz/repos/o/r/branches/main)r   z/repos/o/r/actions/runs/1/rerun)r   z!/repos/o/r/check-runs/1/rerequest)r   z/repos/o/r/pulls/103Npytestraisesr   r   )forbidden_casesr0   r1   s      r)   0test_forbidden_endpoints_dynamic_block_each_caser      sO    O ( 2]]12 	2#FD1	2 	22	2 	2s	   >A	c                     t        j                  t              5  t        dd       d d d        t        j                  t              5  t        dd       d d d        y # 1 sw Y   8xY w# 1 sw Y   y xY w)NGETr   r   r   r:   r+   r)   test_non_post_method_blockedr      sj    	-	. I'GHI	-	. K)IJK KI IK Ks   AA*A'*A3c                    t         dz  dz  j                  d      } g d}| j                  }d} ||      }d}||k(  }|st        j                  d|fd	||f      d
t        j                         v st        j                  |       rt        j                  |       nd
t        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}x}x}}| j                  }d} ||      }d}||k(  }|st        j                  d|fd	||f      d
t        j                         v st        j                  |       rt        j                  |       nd
t        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}x}x}}| j                  }d} ||      }d}||k(  }|st        j                  d|fd	||f      d
t        j                         v st        j                  |       rt        j                  |       nd
t        j                  |      t        j                  |      t        j                  |      t        j                  |      dz  }dd|iz  }t        t        j                  |            dx}x}x}x}}g d}|D ]  }	|	| v}|st        j                  d|fd|	| f      dt        j                         v st        j                  |	      rt        j                  |	      ndd
t        j                         v st        j                  |       rt        j                  |       nd
dz  }
t        j                  d|	       dz   d|
iz  }t        t        j                  |            d} y)u   정적 grep: owner_trigger_only.py 본문에 'pulls/.../merge' 등 금지 string literal 없음.

    유일한 endpoint string 는 ``_build_post_comment_path`` 의 f-string이며
    ``/repos/{owner}/{repo}/issues/{pr_number}/comments`` 만 사용.
    r   r   r   r    )
/pulls/{prz/pulls/{numberpulls/{n}/mergez/git/refs/headsz
/contents/z/labels"z/labels{z/actions/runs/zdef _build_post_comment_path(r   r   )zK%(py6)s
{%(py6)s = %(py2)s
{%(py2)s = %(py0)s.count
}(%(py4)s)
} == %(py9)sr   )rQ   rR   rS   rn   rs   rt   ru   Nz/issues/{pr_number}/commentsr   zreturn f"/repos/)r   r   z/git/refs/heads/{z/contents/{rK   rM   frP   z%forbidden URL builder pattern found: rZ   rS   )r   r\   countr]   r^   r_   r`   ra   rb   rc   rd   re   )r   forbidden_url_buildersrg   r   r   r   r   r   r   r   rh   ri   s               r)   5test_module_source_has_no_call_to_forbidden_endpointsr      s    H$'>>
I
ISZ
I
[C	 99:4:945::5::::5::::::3:::3:::9:::4:::5::::::::::999399349949999499999939993999999939994999999999999-'-9'(-A-(A----(A------3---3---9---'---(---A------- $ I|HHHqHHHHHHqHHHqHHHHHHHHHHHHHDQCHHHHHHHIr+   c                 p   d} t         | k(  }|st        j                  d|fdt         | f      dt        j                         v st        j
                  t               rt        j                  t               ndt        j                  |       dz  }dd|iz  }t        t        j                  |            d x}} y )NOWNER_GEMINI_TRIGGER_TOKENr   z%(py0)s == %(py3)sr
   rQ   rp   r   rq   )	r
   r]   r^   r_   r`   ra   rb   rc   rd   )r   rg   r   r   s       r)   :test_token_env_name_constant_is_owner_gemini_trigger_tokenr      s^    99>99999>9999999>999>99999999999r+   c                    d} | t         v }|st        j                  d|fd| t         f      t        j                  |       dt	        j
                         v st        j                  t               rt        j                  t               nddz  }dd|iz  }t        t        j                  |            d x} }d} | t         v }|st        j                  d|fd| t         f      t        j                  |       dt	        j
                         v st        j                  t               rt        j                  t               nddz  }dd|iz  }t        t        j                  |            d x} }d	} | t         v }|st        j                  d|fd| t         f      t        j                  |       dt	        j
                         v st        j                  t               rt        j                  t               nddz  }dd|iz  }t        t        j                  |            d x} }d
} | t         v }|st        j                  d|fd| t         f      t        j                  |       dt	        j
                         v st        j                  t               rt        j                  t               nddz  }dd|iz  }t        t        j                  |            d x} }y )NBOT_GITHUB_TOKEN)in)z%(py1)s in %(py3)sr   r   r   rq   GH_TOKENGITHUB_TOKEN	OWNER_PAT)	r   r]   r^   rb   r_   r`   ra   rc   rd   )r   r   r   r   s       r)   7test_forbidden_token_env_list_includes_bot_and_gh_tokenr      sy   :!:::::!::::::::::!::::!::::::::2:22222:2222:2222222222222222226>66666>6666>6666666666666666663;33333;3333;333333333333333333r+   c                 z    t        j                  t              5  t        ddi       d d d        y # 1 sw Y   y xY w)Nr   x)r   r   r   r   r:   r+   r)   5test_assert_token_boundary_blocks_owner_pat_injectionr      s1    	-	. 2{C012 2 2s   1:c           	        t        |       }t        |       \  }}}t        j                  t              5  |j                  |ddt        ddi       d d d        g }||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      d	z  }d
d|iz  }t        t        j                  |            d x}}y # 1 sw Y   xY w)NrD   rE   r   z
fallback-xrG   rH   rI   rJ   env_overrider   r   r7   r   r   rq   r*   rB   r   r   r   r[   r"   r]   r^   r_   r`   ra   rb   rc   rd   	r&   rG   rA   r7   rf   r   rg   r   r   s	            r)   test_gh_token_injection_failsr     s    #H-M!(+MC	-	. 
!!' '$l3 	" 	

 5B;5B55B
 
   D  D	c           	        t        |       }t        |       \  }}}t        j                  t              5  |j                  |ddt        ddi       d d d        g }||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      d	z  }d
d|iz  }t        t        j                  |            d x}}y # 1 sw Y   xY w)NrD   rE   r   r   r   r   r   r7   r   r   rq   r   r   s	            r)   !test_github_token_injection_failsr     s    #H-M!(+MC	-	. 
!!' '(#. 	" 	

 5B;5B55B
 
r   c           	        t        |       }t        |       \  }}}t        j                  t              5  |j                  |ddt        ddi       d d d        g }||k(  }|st        j                  d|fd||f      dt        j                         v st        j                  |      rt        j                  |      ndt        j                  |      d	z  }d
d|iz  }t        t        j                  |            d x}}y # 1 sw Y   xY w)NrD   rE   r   zbot-merge-tokenr   r   r   r7   r   r   rq   r   r   s	            r)   %test_bot_github_token_injection_failsr   $  s    #H-M!(+MC	-	. 
!!' ',.?@ 	" 	

 5B;5B55B
 
r   c                 d   t         dz  dz  j                  d      } 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}}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)u*  정적 grep: BOT_GITHUB_TOKEN 환경변수를 owner_trigger_only.py가 사용하지 않음.

    유일한 등장 위치는 FORBIDDEN_TOKEN_ENV_NAMES tuple 와 docstring/주석 (boundary 설명용).
    실제 ``os.environ['BOT_GITHUB_TOKEN']`` / ``os.getenv('BOT_GITHUB_TOKEN')`` 호출 0건.
    r   r   r   r    zos.environ["BOT_GITHUB_TOKEN"]rK   r   r   r   r   rq   Nzos.environ['BOT_GITHUB_TOKEN']zos.getenv("BOT_GITHUB_TOKEN")zos.getenv('BOT_GITHUB_TOKEN')r   r   s        r)   0test_module_source_does_not_use_bot_github_tokenr   2  s    H$'>>
I
ISZ
I
[C+6+36666+3666+666666366636666666+6+36666+3666+666666366636666666*5*#5555*#555*555555#555#5555555*5*#5555*#555*555555#555#5555555r+   c                    t         dz  dz  j                  d      } dD ]  }d| 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| 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| 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| 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 )Nr   r   r   r    )r   r   r   zos.environ["z"]rK   r   r   r   r   rq   zos.environ['z']zos.getenv("z")zos.getenv('z')r   )r   namer   r   r   r   s         r)   9test_module_source_does_not_read_gh_token_or_github_tokenr   ?  s   H$'>>
I
ISZ
I
[C9 1 dV2&1&c1111&c111&111111c111c1111111dV2&1&c1111&c111&111111c111c1111111TF"%0%S0000%S000%000000S000S0000000TF"%0%S0000%S000%000000S000S00000001r+   )r&   r   r   intr@   r   )r&   r   r-   r>   )7__doc__
__future__r   builtinsr_   _pytest.assertion.rewrite	assertionrewriter]   r$   rw   r   syspathlibr   r   __file__resolveparentsr   r>   r1   insertr   r   r   r   r	   r
   r   r   r   anu_v2.owner_trigger_auditr   r"   rN   r*   rB   rj   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r   r:   r+   r)   <module>r      s   	 #     	 
  h'')11!4~chh&HHOOAs>*+	 	 	 9
I  25 & 3C ,J$@*$R&O"22(K#IP:42 
61r+   