Skip to content

Commit 111a704

Browse files
committed
fix columns width on terminal
1 parent fd3acc8 commit 111a704

8 files changed

Lines changed: 351 additions & 122 deletions

File tree

security_overview/cli.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from . import render_md, render_terminal
1010
from .constants import ALL_STATES
1111
from .fetch import check_token, fetch, fetch_pulls
12+
from .render_common import HEADER_COLS
1213

1314
RENDERERS = {
1415
"terminal": render_terminal,
@@ -26,6 +27,16 @@ def parse_states(value):
2627
return states
2728

2829

30+
def parse_columns(value):
31+
cols = [c.strip() for c in value.split(",") if c.strip()]
32+
for c in cols:
33+
if c not in HEADER_COLS:
34+
raise argparse.ArgumentTypeError(
35+
f"invalid column {c!r} (choose from {', '.join(HEADER_COLS)})"
36+
)
37+
return cols
38+
39+
2940
def parse_date(value):
3041
try:
3142
date.fromisoformat(value)
@@ -66,6 +77,16 @@ async def main():
6677
metavar="STATE[,STATE...]",
6778
help="filter by state; repeatable and/or comma-separated (default: all states)",
6879
)
80+
parser.add_argument(
81+
"--columns",
82+
type=parse_columns,
83+
action="append",
84+
metavar="COL[,COL...]",
85+
help=(
86+
"show only these columns; repeatable and/or comma-separated, always "
87+
f"printed in the canonical order (default: all of {', '.join(HEADER_COLS)})"
88+
),
89+
)
6990
parser.add_argument(
7091
"--redact",
7192
action="store_true",
@@ -103,6 +124,7 @@ async def main():
103124
)
104125
args = parser.parse_args()
105126
states = [s for group in (args.state or []) for s in group] or ALL_STATES
127+
columns = [c for group in (args.columns or []) for c in group] or None
106128
renderer = RENDERERS[args.format]
107129

108130
headers = {
@@ -133,13 +155,22 @@ async def main():
133155
for fork_url in fork_urls:
134156
nursery.start_soon(fetch_pulls, client, fork_url, pull_results)
135157

136-
render_kwargs = {"pull_results": pull_results, "redact": args.redact}
158+
render_kwargs = {"pull_results": pull_results, "redact": args.redact, "columns": columns}
137159
if args.format == "terminal":
138160
# tab-separated plain output when piped, so cut/awk/datamash can parse it
139161
plain = not sys.stdout.isatty()
140162
render_kwargs["plain"] = plain
163+
# one width per column for the whole run: every org and the header line
164+
# up in a single table, instead of each org sizing its own columns
165+
widths = None if plain else render_terminal.column_widths(
166+
states, results, pull_results, args.redact, columns
167+
)
168+
render_kwargs["widths"] = widths
141169
# headers go to stderr so stdout stays 1 line = 1 advisory
142-
print(render_terminal.header(redact=args.redact, plain=plain), file=sys.stderr)
170+
print(
171+
render_terminal.header(redact=args.redact, plain=plain, columns=columns, widths=widths),
172+
file=sys.stderr,
173+
)
143174
for org in args.orgs:
144175
out = renderer.render_org(org, states, results, **render_kwargs)
145176
if out:

security_overview/render_common.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,25 @@
33
# Column set shared by the terminal and md renderers. The md renderer carries
44
# org/repo as headings instead of columns, but the rest line up 1:1.
55
HEADER_COLS = ["created", "updated", "to-publish", "state", "org", "repo", "ghsa", "cve", "title", "PRs"]
6+
# what survives --redact: ages and state, nothing that identifies the advisory
7+
REDACT_COLS = ["created", "updated", "to-publish", "state", "org", "repo"]
68
NVD_URL = "https://nvd.nist.gov/vuln/detail/{}"
79

810

11+
def select_columns(columns=None, redact=False):
12+
"""The columns to show, always in HEADER_COLS order.
13+
14+
`columns` is the --columns allow-list (None means all). Redaction is applied
15+
last, so it wins over anything the caller asked for. Both renderers derive
16+
their header and their cells from this one list, which is what keeps the two
17+
in step and keeps rows matching the header.
18+
"""
19+
cols = HEADER_COLS if columns is None else [c for c in HEADER_COLS if c in columns]
20+
if redact:
21+
cols = [c for c in cols if c in REDACT_COLS]
22+
return cols
23+
24+
925
def time_to_publish(advisory):
1026
"""Days from creation to publication; None unless the advisory is published."""
1127
if advisory.get("state") != "published":

security_overview/render_md.py

Lines changed: 24 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,31 @@
11
from .render_common import (
2-
HEADER_COLS,
32
NVD_URL,
43
days_ago,
54
group_by_repo,
65
pr_label,
6+
select_columns,
77
time_to_publish,
88
)
99

10-
# org/repo are headings in markdown, so they drop out of the table itself
11-
_COLS = [c for c in HEADER_COLS if c not in ("org", "repo")]
12-
# redaction strips advisory-identifying columns, same as the terminal renderer
13-
_REDACTED_COLS = _COLS[: _COLS.index("state") + 1]
14-
1510

1611
def _cell(text):
1712
"""Escape what would otherwise break out of a table cell."""
1813
return str(text).replace("|", "\\|").replace("\n", " ")
1914

2015

21-
def render_org(org, states, results_by_key, pull_results=None, redact=False):
16+
def render_org(org, states, results_by_key, pull_results=None, redact=False, columns=None):
2217
advisories = []
2318
for state in states:
2419
advisories.extend(results_by_key.get((org, state), []))
2520

2621
if not advisories:
2722
return ""
2823

29-
cols = _REDACTED_COLS if redact else _COLS
24+
# org/repo are headings in markdown, so they never appear as columns
25+
cols = [c for c in select_columns(columns, redact) if c not in ("org", "repo")]
26+
if not cols:
27+
return ""
28+
3029
header = "| " + " | ".join(cols) + " |"
3130
divider = "|" + "|".join("-" * (len(c) + 2) for c in cols) + "|"
3231

@@ -39,27 +38,22 @@ def render_org(org, states, results_by_key, pull_results=None, redact=False):
3938
lines.append(divider)
4039
for advisory in items:
4140
ttp = time_to_publish(advisory)
42-
cells = [
43-
days_ago(advisory.get("created_at", "")),
44-
days_ago(advisory.get("updated_at", "")),
45-
"" if ttp is None else ttp,
46-
advisory.get("state") or "?",
47-
]
48-
49-
if not redact:
50-
url = advisory.get("html_url", "")
51-
ghsa_id = advisory.get("ghsa_id") or ""
52-
cve = advisory.get("cve_id") or ""
53-
fork = advisory.get("private_fork")
54-
fork_html_url = fork.get("html_url") if fork else None
55-
pulls = (pull_results or {}).get(fork_html_url, []) if fork_html_url else []
56-
cells += [
57-
f"[{ghsa_id}]({url})" if (ghsa_id and url) else ghsa_id,
58-
f"[{cve}]({NVD_URL.format(cve)})" if cve else "",
59-
_cell(advisory.get("summary") or ""),
60-
", ".join(f"[{pr_label(p)}]({p['html_url']})" for p in pulls),
61-
]
62-
63-
lines.append("| " + " | ".join(str(c) for c in cells) + " |")
41+
url = advisory.get("html_url", "")
42+
ghsa_id = advisory.get("ghsa_id") or ""
43+
cve = advisory.get("cve_id") or ""
44+
fork = advisory.get("private_fork")
45+
fork_html_url = fork.get("html_url") if fork else None
46+
pulls = (pull_results or {}).get(fork_html_url, []) if fork_html_url else []
47+
cell = {
48+
"created": days_ago(advisory.get("created_at", "")),
49+
"updated": days_ago(advisory.get("updated_at", "")),
50+
"to-publish": "" if ttp is None else ttp,
51+
"state": advisory.get("state") or "?",
52+
"ghsa": f"[{ghsa_id}]({url})" if (ghsa_id and url) else ghsa_id,
53+
"cve": f"[{cve}]({NVD_URL.format(cve)})" if cve else "",
54+
"title": _cell(advisory.get("summary") or ""),
55+
"PRs": ", ".join(f"[{pr_label(p)}]({p['html_url']})" for p in pulls),
56+
}
57+
lines.append("| " + " | ".join(str(cell[c]) for c in cols) + " |")
6458

6559
return "\n".join(lines)

0 commit comments

Comments
 (0)