-
Notifications
You must be signed in to change notification settings - Fork 52
Expand file tree
/
Copy pathposture-summary
More file actions
executable file
·155 lines (141 loc) · 4.82 KB
/
Copy pathposture-summary
File metadata and controls
executable file
·155 lines (141 loc) · 4.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#!/bin/sh
# This file is part of KASLD - https://github.com/bcoles/kasld
#
# posture-summary — roll up many `kasld -j` snapshots into one table, one row
# per host. The fleet view: scan the whole estate's KASLR posture at a glance
# instead of reading N separate reports.
#
# Input is one `kasld -j` file per host, passed as arguments. The host label is
# the file's basename (minus a .json suffix), because `kasld -j` deliberately
# carries no hostname — identity comes from how you name the file when you
# collect it. This tool does NO collection and NO transport: your own fan-out
# supplies the snapshots. A reference loop:
#
# for h in $(cat hosts); do ssh "$h" 'kasld -j' > "snap/$h.json"; done
# posture-summary snap/*.json
#
# (or replay extra/collect bundles: KASLD_SYSROOT=<bundle>/sysroot kasld -j).
#
# A file that is not a valid `kasld -j` snapshot is skipped with a warning
# (one bad host does not sink the report); the skipped count is noted.
#
# Only the boot-STABLE posture is summarised — the per-boot resolved addresses
# never appear (they re-randomize every boot and mean nothing across hosts).
#
# Usage: posture-summary [--text|--markdown|--csv|--json] FILE...
# Exit: 0 = at least one snapshot summarised, 2 = usage / no valid input.
#
# Dependencies: jq, awk.
# ---
# <bcoles@gmail.com>
set -u
prog=$(basename "$0")
fmt=text
usage() {
cat <<EOF
Usage: $prog [FORMAT] FILE...
Summarise many 'kasld -j' snapshots (one file per host) into one table.
Host label = file basename. Bad snapshots are skipped with a warning.
FORMAT:
--text aligned text table (default)
--markdown GitHub/GitLab markdown table
--csv comma-separated (spreadsheets)
--json array of per-host posture objects
-h, --help this help
Exit: 0 summarised >=1 host, 2 usage / no valid input.
EOF
}
while [ $# -gt 0 ]; do
case "$1" in
--text) fmt=text ;;
--markdown | --md) fmt=markdown ;;
--csv) fmt=csv ;;
--json) fmt=json ;;
-h | --help)
usage
exit 0
;;
--)
shift
break
;;
-*)
echo "$prog: unknown option: $1" >&2
exit 2
;;
*) break ;;
esac
shift
done
[ $# -ge 1 ] || {
usage >&2
exit 2
}
command -v jq >/dev/null 2>&1 || {
echo "$prog: jq not found (apt install jq)" >&2
exit 2
}
# Per-host posture object. Boot-stable fields only; the resolved base address /
# slide are never read. Missing blocks (older -j, KASLR off) coalesce safely.
# $host is a jq variable (bound with --arg below), not shell expansion.
# shellcheck disable=SC2016
row='{
host: $host,
arch: (.arch // "?"),
kernel: (.kernel.release // "?"),
kaslr: (.hardening.kaslr_posture.state // "active"),
vbits: (.kaslr.inferred.entropy_bits // 0),
pbits: (.kaslr.inferred_physical.entropy_bits // 0),
leaks: (.hardening.exposure.succeeded // 0),
total: (.hardening.exposure.total // 0),
defenses: ([(.hardening.confirmed_mitigations // [])[].gate] | unique | length),
cves: ((.hardening.patched_vulnerabilities.possibly_unpatched // []) | length),
topfix: ((.hardening.available_hardening // []) | (if length > 0 then .[0].action else "-" end))
}'
tmp=$(mktemp) || exit 2
trap 'rm -f "$tmp"' EXIT
skipped=0
for f in "$@"; do
if [ ! -f "$f" ] || ! jq -e '((.kaslr | type) == "object") and ((.hardening | type) == "object")' \
"$f" >/dev/null 2>&1; then
echo "$prog: skipping (not a kasld -j snapshot): $f" >&2
skipped=$((skipped + 1))
continue
fi
host=$(basename "$f" .json)
jq -c --arg host "$host" "$row" "$f" >>"$tmp"
done
[ -s "$tmp" ] || {
echo "$prog: no valid snapshots" >&2
exit 2
}
HDR="host arch kernel kaslr vbits pbits leaks defenses cves top-fix"
case "$fmt" in
json)
jq -s 'sort_by(.host)' "$tmp"
;;
csv)
echo "$HDR" | tr ' ' ','
jq -rs 'sort_by(.host)[] | [.host, .arch, .kernel, .kaslr, .vbits, .pbits,
"\(.leaks)/\(.total)", .defenses, .cves, .topfix] | @csv' "$tmp"
;;
markdown)
echo "$HDR" | sed 's/ / | /g; s/^/| /; s/$/ |/'
echo "$HDR" | sed 's/[^ ]*/---/g; s/ / | /g; s/^/| /; s/$/ |/'
jq -rs 'sort_by(.host)[] | "| \(.host) | \(.arch) | \(.kernel) | \(.kaslr) | \(.vbits) | \(.pbits) | \(.leaks)/\(.total) | \(.defenses) | \(.cves) | \(.topfix) |"' "$tmp"
;;
text)
# Header + rows as tab-separated, then align each column to its widest cell.
{
echo "$HDR" | tr ' ' '\t'
jq -rs 'sort_by(.host)[] | [.host, .arch, .kernel, .kaslr, "\(.vbits)b",
"\(.pbits)b", "\(.leaks)/\(.total)", .defenses, .cves, .topfix] | @tsv' "$tmp"
} | awk -F'\t' '
{ for (i = 1; i <= NF; i++) { c[NR, i] = $i; if (length($i) > w[i]) w[i] = length($i) } nc = NF; nr = NR }
END { for (r = 1; r <= nr; r++) { line = ""
for (i = 1; i <= nc; i++) { line = line sprintf("%-*s", w[i] + 2, c[r, i]) }
sub(/[ \t]+$/, "", line); print line } }'
;;
esac
[ "$skipped" -eq 0 ] || echo "$prog: skipped $skipped file(s)" >&2
exit 0