55
66import argparse
77import json
8- import os
9- import stat
108import sys
119from pathlib import Path
1210
13- MAX_SECURITY_MD_BYTES = 1024 * 1024
14-
1511
1612class ResolutionError (ValueError ):
1713 """Raised when a SECURITY.md chain cannot be resolved."""
@@ -24,50 +20,14 @@ def _inside(path: Path, root: Path, label: str) -> Path:
2420 raise ResolutionError (f"{ label } is outside the scan root: { path } " ) from exc
2521
2622
27- def _resolve_root (repo : Path ) -> Path :
23+ def resolve_security_md (repo : Path , scope : Path ) -> str :
24+ """Return applicable SECURITY.md files, concatenated root to leaf."""
2825 try :
2926 root = repo .expanduser ().resolve (strict = True )
3027 except OSError as exc :
3128 raise ResolutionError (f"scan root does not exist: { repo } " ) from exc
3229 if not root .is_dir ():
3330 raise ResolutionError (f"scan root is not a directory: { root } " )
34- return root
35-
36-
37- def list_security_md (repo : Path ) -> list [str ]:
38- """Return a stable, safely framed inventory without traversing Git metadata."""
39- root = _resolve_root (repo )
40-
41- def raise_walk_error (error : OSError ) -> None :
42- raise error
43-
44- policies : list [str ] = []
45- for directory , subdirectories , filenames in os .walk (
46- root , onerror = raise_walk_error , followlinks = False
47- ):
48- safe_subdirectories : list [str ] = []
49- for name in sorted (subdirectories ):
50- if name == ".git" :
51- continue
52- directory_stat = (Path (directory ) / name ).stat (follow_symlinks = False )
53- if not stat .S_ISDIR (directory_stat .st_mode ):
54- continue
55- reparse_point = getattr (stat , "FILE_ATTRIBUTE_REPARSE_POINT" , 0 )
56- if getattr (directory_stat , "st_file_attributes" , 0 ) & reparse_point :
57- continue
58- safe_subdirectories .append (name )
59- subdirectories [:] = safe_subdirectories
60- if "SECURITY.md" not in filenames :
61- continue
62- policy = Path (directory ) / "SECURITY.md"
63- if policy .is_file () or policy .is_symlink ():
64- policies .append (policy .relative_to (root ).as_posix ())
65- return sorted (policies )
66-
67-
68- def resolve_security_md (repo : Path , scope : Path ) -> str :
69- """Return applicable SECURITY.md files, concatenated root to leaf."""
70- root = _resolve_root (repo )
7131
7232 requested_scope = scope .expanduser ()
7333 if not requested_scope .is_absolute ():
@@ -94,11 +54,7 @@ def resolve_security_md(repo: Path, scope: Path) -> str:
9454 resolved_policy = policy .resolve (strict = True )
9555 _inside (resolved_policy , root , "SECURITY.md" )
9656 try :
97- with resolved_policy .open ("rb" ) as policy_file :
98- policy_bytes = policy_file .read (MAX_SECURITY_MD_BYTES + 1 )
99- if len (policy_bytes ) > MAX_SECURITY_MD_BYTES :
100- raise ResolutionError (f"SECURITY.md exceeds 1 MiB: { policy } " )
101- content = policy_bytes .decode ("utf-8" )
57+ content = policy .read_bytes ().decode ("utf-8" )
10258 except UnicodeDecodeError as exc :
10359 raise ResolutionError (f"SECURITY.md is not valid UTF-8: { policy } " ) from exc
10460 if not content .strip ():
@@ -116,35 +72,22 @@ def resolve_security_md(repo: Path, scope: Path) -> str:
11672def parse_args () -> argparse .Namespace :
11773 parser = argparse .ArgumentParser (description = __doc__ )
11874 parser .add_argument ("--repo" , required = True , type = Path , help = "scan root directory" )
119- parser .add_argument (
120- "--list" ,
121- action = "store_true" ,
122- help = "write a JSON inventory of repository policy paths" ,
123- )
12475 parser .add_argument (
12576 "--scope" ,
77+ required = True ,
12678 type = Path ,
12779 help = "existing file or directory within the scan root" ,
12880 )
129- parser .add_argument ("--out" , default = Path ("-" ), type = Path , help = "output path, or - for stdout" )
130- args = parser .parse_args ()
131- if args .list and args .scope is not None :
132- parser .error ("--list cannot be combined with --scope" )
133- if not args .list and args .scope is None :
134- parser .error ("--scope is required unless --list is specified" )
135- return args
81+ parser .add_argument ("--out" , required = True , type = Path , help = "output Markdown path, or -" )
82+ return parser .parse_args ()
13683
13784
13885def main () -> int :
13986 args = parse_args ()
14087 try :
141- guidance = (
142- json .dumps (list_security_md (args .repo ), ensure_ascii = True ) + "\n "
143- if args .list
144- else resolve_security_md (args .repo , args .scope )
145- )
88+ guidance = resolve_security_md (args .repo , args .scope )
14689 if args .out == Path ("-" ):
147- sys .stdout .buffer . write (guidance . encode ( "utf-8" ) )
90+ sys .stdout .write (guidance )
14891 else :
14992 args .out .parent .mkdir (parents = True , exist_ok = True )
15093 args .out .write_text (guidance , encoding = "utf-8" )
0 commit comments