11import argparse
2+ import os
3+ import sys
4+ from datetime import date
25
36import httpx
47import trio
@@ -23,6 +26,35 @@ def parse_states(value):
2326 return states
2427
2528
29+ def parse_date (value ):
30+ try :
31+ date .fromisoformat (value )
32+ except ValueError :
33+ raise argparse .ArgumentTypeError (f"invalid date { value !r} (expected YYYY-MM-DD)" )
34+ return value
35+
36+
37+ def filter_date_range (results , field , date_from , date_to ):
38+ """Keep advisories whose `field` date falls in [date_from, date_to] (inclusive).
39+
40+ Advisories missing the field (e.g. never published) are dropped when a bound is set.
41+ """
42+ if not date_from and not date_to :
43+ return results
44+
45+ def keep (advisory ):
46+ value = (advisory .get (field ) or "" )[:10 ]
47+ if not value :
48+ return False
49+ if date_from and value < date_from :
50+ return False
51+ if date_to and value > date_to :
52+ return False
53+ return True
54+
55+ return {key : [a for a in advisories if keep (a )] for key , advisories in results .items ()}
56+
57+
2658async def main ():
2759 token = check_token ()
2860 parser = argparse .ArgumentParser (prog = "security-overview" )
@@ -45,6 +77,30 @@ async def main():
4577 default = "terminal" ,
4678 help = "output format (default: terminal)" ,
4779 )
80+ parser .add_argument (
81+ "--opened-from" ,
82+ type = parse_date ,
83+ metavar = "YYYY-MM-DD" ,
84+ help = "only advisories opened on or after this date" ,
85+ )
86+ parser .add_argument (
87+ "--opened-to" ,
88+ type = parse_date ,
89+ metavar = "YYYY-MM-DD" ,
90+ help = "only advisories opened on or before this date" ,
91+ )
92+ parser .add_argument (
93+ "--published-from" ,
94+ type = parse_date ,
95+ metavar = "YYYY-MM-DD" ,
96+ help = "only advisories published on or after this date" ,
97+ )
98+ parser .add_argument (
99+ "--published-to" ,
100+ type = parse_date ,
101+ metavar = "YYYY-MM-DD" ,
102+ help = "only advisories published on or before this date" ,
103+ )
48104 args = parser .parse_args ()
49105 states = [s for group in (args .state or []) for s in group ] or ALL_STATES
50106 renderer = RENDERERS [args .format ]
@@ -64,6 +120,9 @@ async def main():
64120 for state in states :
65121 nursery .start_soon (fetch , client , org , state , results )
66122
123+ results = filter_date_range (results , "created_at" , args .opened_from , args .opened_to )
124+ results = filter_date_range (results , "published_at" , args .published_from , args .published_to )
125+
67126 fork_urls = {
68127 advisory ["private_fork" ]["html_url" ]
69128 for advisories in results .values ()
@@ -74,13 +133,28 @@ async def main():
74133 for fork_url in fork_urls :
75134 nursery .start_soon (fetch_pulls , client , fork_url , pull_results )
76135
136+ render_kwargs = {"pull_results" : pull_results , "redact" : args .redact }
137+ if args .format == "terminal" :
138+ # tab-separated plain output when piped, so cut/awk/datamash can parse it
139+ plain = not sys .stdout .isatty ()
140+ render_kwargs ["plain" ] = plain
141+ # headers go to stderr so stdout stays 1 line = 1 advisory
142+ print (render_terminal .header (redact = args .redact , plain = plain ), file = sys .stderr )
77143 for org in args .orgs :
78- out = renderer .render_org (org , states , results , pull_results = pull_results , redact = args . redact )
144+ out = renderer .render_org (org , states , results , ** render_kwargs )
79145 if out :
80146 print (out )
81- print ()
147+ # terminal format stays 1 line = 1 advisory so `wc -l` counts vulns
148+ if args .format == "md" :
149+ print ()
82150
83151
84152def run ():
85153 """Synchronous console-script entry point."""
86- trio .run (main )
154+ try :
155+ trio .run (main )
156+ except BrokenPipeError :
157+ # downstream closed the pipe (e.g. `| head`); redirect stdout to
158+ # devnull so the interpreter's exit flush doesn't error too
159+ os .dup2 (os .open (os .devnull , os .O_WRONLY ), sys .stdout .fileno ())
160+ sys .exit (141 ) # 128 + SIGPIPE
0 commit comments