WebDiensteNginxSecurityHeaderScript: nginx-header-diff.sh

File nginx-header-diff.sh, 10.6 KB (added by Johannes Lode, 12 days ago)
Line 
1#!/bin/bash
2# ---------------------------------------------------------------------------
3# nginx-header-diff.sh
4#
5# Vergleicht die add_header-Direktiven einer NGINX-.conf-Datei mit dem
6# effektiven Header-Satz einer Include-Datei (Includes werden rekursiv
7# aufgeloest) und meldet je Block, was doppelt, was abweichend und was nur
8# in der .conf steht.
9#
10# Aufruf:
11#   nginx-header-diff.sh [-p PREFIX] [-d DIREKTIVE] <datei.conf> <datei.inc>
12#
13#   -p PREFIX     Basisverzeichnis fuer relative include-Pfade.
14#                 Vorgabe: /etc/nginx
15#   -d DIREKTIVE  Zusaetzlich zu beruecksichtigende Direktive, mehrfach
16#                 angebbar.  Vorgabe: nur add_header.
17#                 Sinnvoll z.B. -d proxy_hide_header.
18#
19# Ausgabeformat der Fundstellen entspricht grep -n:  datei:zeile: text
20#
21# Markierungen:
22#   [DUPLIKAT]  Name und Wert identisch mit dem Include  -> Zeile loeschen
23#   [KONFLIKT]  Name gleich, Wert abweichend             -> entscheiden
24#   [NUR-CONF]  im Include nicht enthalten               -> Zeile behalten
25#
26# Grenzen:
27#   - Vollzeilen-Kommentare werden ignoriert, ebenso Kommentare am
28#     Zeilenende ausserhalb von Anfuehrungszeichen.
29#   - Direktiven ueber mehrere Zeilen werden zusammengefasst, solange die
30#     Fortsetzungszeilen kein ; { } enthalten.
31#   - Geschweifte Klammern innerhalb von Direktiv-Werten (z.B. in
32#     log_format) koennen die Blockerkennung stoeren.
33#   - include-Pfade mit Platzhaltern (conf.d/*.inc) werden nicht aufgeloest.
34# ---------------------------------------------------------------------------
35
36set -u
37
38prefix=/etc/nginx
39directives=(add_header)
40
41usage() {
42    sed -n '3,30p' "$0" | sed 's/^# \{0,1\}//'
43    exit "${1:-1}"
44}
45
46while getopts ':p:d:h' opt; do
47    case "$opt" in
48        p) prefix=$OPTARG ;;
49        d) directives+=("$OPTARG") ;;
50        h) usage 0 ;;
51        :) echo "Fehlendes Argument zu -$OPTARG" >&2; exit 2 ;;
52        \?) echo "Unbekannte Option -$OPTARG" >&2; exit 2 ;;
53    esac
54done
55shift $((OPTIND - 1))
56
57[ $# -eq 2 ] || usage 2
58
59conf=$1
60inc=$2
61
62for f in "$conf" "$inc"; do
63    [ -r "$f" ] || { echo "Nicht lesbar: $f" >&2; exit 2; }
64done
65
66dirlist=$(IFS='|'; echo "${directives[*]}")
67
68# ---------------------------------------------------------------------------
69# Gemeinsamer Scanner.  Gibt je logischer Direktive eine Zeile aus:
70#   D <tab> startzeile <tab> direktive <tab> rest
71#   O <tab> startzeile <tab> blocktext        (Blockanfang)
72#   C <tab> startzeile                        (Blockende)
73# ---------------------------------------------------------------------------
74scan_awk='
75function trim(s) { gsub(/^[ \t\r]+|[ \t\r]+$/, "", s); gsub(/[ \t]+/, " ", s); return s }
76
77function emit(stmt,   d, rest) {
78    stmt = trim(stmt)
79    if (stmt == "") return
80    d = stmt
81    sub(/[ \t].*$/, "", d)
82    rest = stmt
83    sub(/^[^ \t]+[ \t]*/, "", rest)
84    printf "D\t%d\t%s\t%s\n", startLine, d, trim(rest)
85}
86
87# Zeichenweise, damit ; { } innerhalb von Anfuehrungszeichen die Direktive
88# nicht zerreissen (z.B. "max-age=63072000; includeSubDomains").
89{
90    len = length($0)
91    for (i = 1; i <= len; i++) {
92        c = substr($0, i, 1)
93
94        if (q != "") {                      # innerhalb eines Strings
95            buf = buf c
96            if (c == q) q = ""
97            continue
98        }
99
100        if (c == "#") break                 # Rest der Zeile ist Kommentar
101
102        if (c == "\"" || c == "\x27") {
103            if (trim(buf) == "") startLine = NR
104            buf = buf c; q = c
105            continue
106        }
107
108        if (c == ";") { emit(buf); buf = ""; continue }
109
110        if (c == "{") {
111            printf "O\t%d\t%s\n", (trim(buf) == "" ? NR : startLine), trim(buf)
112            buf = ""
113            continue
114        }
115
116        if (c == "}") {
117            if (trim(buf) != "") emit(buf)
118            printf "C\t%d\n", NR
119            buf = ""
120            continue
121        }
122
123        if (trim(buf) == "") {
124            if (c == " " || c == "\t" || c == "\r") continue
125            startLine = NR
126        }
127        buf = buf c
128    }
129    buf = buf " "                           # Zeilenumbruch als Worttrenner
130}
131END { emit(buf) }
132'
133
134# ---------------------------------------------------------------------------
135# Header-Wert normalisieren: Anfuehrungszeichen entfernen, Whitespace
136# zusammenfassen, "always" als eigenes Merkmal fuehren.
137# ---------------------------------------------------------------------------
138norm_awk='
139function normValue(v,   n, i, tok, out, always, arr) {
140    always = 0
141    if (v ~ /[ \t]always$/ || v == "always") { always = 1; sub(/[ \t]*always$/, "", v) }
142    gsub(/^[ \t]+|[ \t]+$/, "", v)
143    n = split(v, arr, /[ \t]+/)
144    out = ""
145    for (i = 1; i <= n; i++) {
146        tok = arr[i]
147        gsub(/^["\x27]|["\x27]$/, "", tok)
148        out = out (out == "" ? "" : " ") tok
149    }
150    gsub(/[ \t]*;[ \t]*/, "; ", out)
151    gsub(/[ \t]+/, " ", out)
152    return out (always ? " +always" : "")
153}
154'
155
156# ---------------------------------------------------------------------------
157# Includes rekursiv aufloesen und die effektiven Header einsammeln.
158# ---------------------------------------------------------------------------
159tmp=$(mktemp -d) || exit 1
160trap 'rm -rf "$tmp"' EXIT
161
162seen_file=$tmp/seen
163: >"$seen_file"
164: >"$tmp/inc-headers"
165
166resolve() {
167    local file=$1 depth=$2
168    local real
169    real=$(readlink -f -- "$file" 2>/dev/null || echo "$file")
170
171    if grep -qxF -- "$real" "$seen_file"; then
172        echo "Warnung: Include-Zyklus bei $file uebersprungen" >&2
173        return
174    fi
175    echo "$real" >>"$seen_file"
176
177    if [ ! -r "$file" ]; then
178        echo "Warnung: Include nicht lesbar: $file" >&2
179        return
180    fi
181    if [ "$depth" -gt 16 ]; then
182        echo "Warnung: Include-Tiefe > 16 bei $file" >&2
183        return
184    fi
185
186    local type line directive rest target
187    while IFS=$'\t' read -r type line directive rest; do
188        [ "$type" = "D" ] || continue
189        case "$directive" in
190            include)
191                target=${rest%;}
192                target=$(printf '%s' "$target" | tr -d "\"'" )
193                target=$(printf '%s' "$target" | sed 's/[[:space:]]*$//')
194                case "$target" in
195                    /*) : ;;
196                    *)  target=$prefix/$target ;;
197                esac
198                resolve "$target" $((depth + 1))
199                ;;
200            *)
201                printf '%s\t%s\t%s\t%s\n' "$directive" "$rest" "$file" "$line" \
202                    >>"$tmp/inc-headers"
203                ;;
204        esac
205    done < <(awk "$scan_awk" "$file")
206}
207
208resolve "$inc" 0
209
210# Nur die interessierenden Direktiven behalten und normalisieren.
211awk -F'\t' -v dirs="$dirlist" "$norm_awk"'
212BEGIN { n = split(dirs, d, "|"); for (i = 1; i <= n; i++) want[d[i]] = 1 }
213{
214    if (!($1 in want)) next
215    rest = $2
216    name = rest
217    sub(/[ \t].*$/, "", name)
218    value = rest
219    sub(/^[^ \t]+[ \t]*/, "", value)
220    # Feld 6: Originalschreibweise wie in der Datei, nur Whitespace normiert
221    printf "%s\t%s\t%s\t%s\t%s\t%s\n", $1, tolower(name), normValue(value), $3, $4, rest
222}' "$tmp/inc-headers" >"$tmp/inc-norm"
223
224# ---------------------------------------------------------------------------
225# Bericht
226# ---------------------------------------------------------------------------
227echo "=== Effektiver Header-Satz aus $inc ==="
228if [ ! -s "$tmp/inc-norm" ]; then
229    echo "  (keine passenden Direktiven gefunden)"
230else
231    awk -F'\t' '{ printf "  %-66s  [%s:%s]\n", $1 " " $6 ";", $4, $5 }' \
232        "$tmp/inc-norm"
233fi
234echo
235
236awk "$scan_awk" "$conf" \
237| awk -F'\t' -v conf="$conf" -v inc="$inc" -v dirs="$dirlist" \
238      -v incfile="$tmp/inc-norm" "$norm_awk"'
239BEGIN {
240    n = split(dirs, d, "|"); for (i = 1; i <= n; i++) want[d[i]] = 1
241
242    while ((getline l < incfile) > 0) {
243        split(l, f, "\t")
244        key = f[1] "\t" f[2]
245        incName[key] = 1
246        incPair[key "\t" f[3]] = 1
247        # Anzeige in der Originalschreibweise der Include-Datei
248        incVals[key] = incVals[key] (incVals[key] == "" ? "" : " | ") f[1] " " f[6] ";"
249    }
250    close(incfile)
251
252    # Basisname der Include-Datei fuer den Abgleich der include-Zeilen
253    incBase = inc
254    sub(/^.*\//, "", incBase)
255
256    depth = 0
257    blockCount = 0
258}
259
260function blockLabel(i) { return blockText[i] }
261
262$1 == "O" {
263    depth++
264    blockCount++
265    stack[depth] = blockCount
266    blockText[blockCount] = ($3 == "" ? "(anonym)" : $3)
267    blockLine[blockCount] = $2
268    blockDepth[blockCount] = depth
269    next
270}
271
272$1 == "C" {
273    if (depth > 0) depth--
274    next
275}
276
277$1 == "D" {
278    cur = (depth > 0 ? stack[depth] : 0)
279    if (cur == 0 && blockText[0] == "") {
280        blockText[0] = "(Dateiebene)"
281        blockLine[0] = 1
282        blockDepth[0] = 0
283    }
284
285    if ($3 == "include") {
286        t = $4
287        sub(/[ \t]*;?[ \t]*$/, "", t)
288        gsub(/["\x27]/, "", t)
289        b = t
290        sub(/^.*\//, "", b)
291        if (b == incBase) hasInc[cur] = 1
292        else otherInc[cur] = otherInc[cur] (otherInc[cur] == "" ? "" : ", ") b
293        next
294    }
295
296    if (!($3 in want)) next
297
298    rest = $4
299    sub(/[ \t]*;[ \t]*$/, "", rest)
300    name = rest
301    sub(/[ \t].*$/, "", name)
302    value = rest
303    sub(/^[^ \t]+[ \t]*/, "", value)
304
305    lname = tolower(name)
306    key   = $3 "\t" lname
307    nv    = normValue(value)
308
309    if ((key "\t" nv) in incPair)      mark = "[DUPLIKAT]"
310    else if (key in incName)           mark = "[KONFLIKT]"
311    else                               mark = "[NUR-CONF]"
312
313    hdrCount[cur]++
314    i = hdrCount[cur]
315    hLine[cur, i] = $2
316    hText[cur, i] = $3 " " rest ";"
317    hMark[cur, i] = mark
318    hAlt [cur, i] = (mark == "[KONFLIKT]" ? incVals[key] : "")
319
320    if (mark == "[DUPLIKAT]") nDup++
321    else if (mark == "[KONFLIKT]") nCon++
322    else nOnly++
323    next
324}
325
326END {
327    print "=== " conf " ==="
328    print ""
329
330    any = 0
331    for (b = 0; b <= blockCount; b++) {
332        if (hdrCount[b] == 0) continue
333        any = 1
334
335        indent = ""
336        for (k = 1; k < blockDepth[b]; k++) indent = indent "  "
337
338        printf "%s%s   (Zeile %s)\n", indent, blockLabel(b), blockLine[b]
339        if (hasInc[b])
340            printf "%s  include %s: vorhanden\n", indent, incBase
341        else
342            printf "%s  include %s: FEHLT -> einfuegen\n", indent, incBase
343        if (otherInc[b] != "")
344            printf "%s  weitere includes: %s\n", indent, otherInc[b]
345
346        for (i = 1; i <= hdrCount[b]; i++) {
347            printf "%s  %s:%s: %s  %s\n", indent, conf, hLine[b, i], hText[b, i], hMark[b, i]
348            if (hAlt[b, i] != "")
349                printf "%s      Include hat: %s\n", indent, hAlt[b, i]
350        }
351        print ""
352    }
353
354    if (!any) print "(keine passenden Direktiven gefunden)"
355
356    print "---"
357    printf "DUPLIKAT %d   KONFLIKT %d   NUR-CONF %d\n", nDup+0, nCon+0, nOnly+0
358    if (nCon > 0)
359        print "Hinweis: KONFLIKT-Zeilen von Hand entscheiden, bevor etwas geloescht wird."
360}
361'