feat: filter apt sources down to security suites for security-only patching
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
package updates
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// isSecuritySuite reports whether an apt suite carries security fixes. Debian
|
||||
// 11+ and every supported Ubuntu name them "<codename>-security".
|
||||
func isSecuritySuite(s string) bool { return strings.HasSuffix(s, "-security") }
|
||||
|
||||
// securitySources reduces a host's apt source files to only the entries that
|
||||
// point at a security suite, so an upgrade run against them installs security
|
||||
// fixes and nothing else.
|
||||
//
|
||||
// It is a pure function of file contents so it is tested on any platform. The
|
||||
// caller writes list to a *.list file and deb822 to a *.sources file in a
|
||||
// temporary SourceParts directory: keeping deb822 paragraphs as deb822 means
|
||||
// an inline Signed-By key block survives verbatim, which a conversion to
|
||||
// one-line format could not carry.
|
||||
//
|
||||
// ok is false when no security suite exists at all. The caller must then
|
||||
// report unsupported, never fall back to installing everything.
|
||||
func securitySources(files map[string]string) (list string, deb822 string, ok bool) {
|
||||
paths := make([]string, 0, len(files))
|
||||
for p := range files {
|
||||
paths = append(paths, p)
|
||||
}
|
||||
sort.Strings(paths) // deterministic output
|
||||
|
||||
var lb, db strings.Builder
|
||||
for _, p := range paths {
|
||||
if strings.HasSuffix(p, ".sources") {
|
||||
db.WriteString(filterDeb822(files[p]))
|
||||
} else {
|
||||
lb.WriteString(filterOneLine(files[p]))
|
||||
}
|
||||
}
|
||||
list, deb822 = lb.String(), db.String()
|
||||
return list, deb822, list != "" || deb822 != ""
|
||||
}
|
||||
|
||||
func filterOneLine(content string) string {
|
||||
var b strings.Builder
|
||||
for _, raw := range strings.Split(content, "\n") {
|
||||
line := strings.TrimSpace(raw)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 3 || fields[0] != "deb" {
|
||||
continue
|
||||
}
|
||||
i := 1
|
||||
if strings.HasPrefix(fields[i], "[") {
|
||||
// Options run until the token that closes the bracket.
|
||||
for i < len(fields) && !strings.HasSuffix(fields[i], "]") {
|
||||
i++
|
||||
}
|
||||
i++
|
||||
}
|
||||
// fields[i] is the URI, fields[i+1] the suite.
|
||||
if i+1 < len(fields) && isSecuritySuite(fields[i+1]) {
|
||||
b.WriteString(line)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
func filterDeb822(content string) string {
|
||||
var b strings.Builder
|
||||
for _, para := range strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n\n") {
|
||||
lines := strings.Split(strings.Trim(para, "\n"), "\n")
|
||||
var out []string
|
||||
isDeb, enabled, kept := false, true, false
|
||||
for _, l := range lines {
|
||||
key, val, found := strings.Cut(l, ":")
|
||||
k := strings.ToLower(strings.TrimSpace(key))
|
||||
v := strings.TrimSpace(val)
|
||||
switch {
|
||||
case found && k == "types":
|
||||
for _, t := range strings.Fields(v) {
|
||||
if t == "deb" {
|
||||
isDeb = true
|
||||
}
|
||||
}
|
||||
case found && k == "enabled":
|
||||
enabled = strings.ToLower(v) != "no"
|
||||
case found && k == "suites":
|
||||
var sec []string
|
||||
for _, s := range strings.Fields(v) {
|
||||
if isSecuritySuite(s) {
|
||||
sec = append(sec, s)
|
||||
}
|
||||
}
|
||||
if len(sec) == 0 {
|
||||
continue // drop the line; the paragraph is dropped below
|
||||
}
|
||||
kept = true
|
||||
l = "Suites: " + strings.Join(sec, " ")
|
||||
}
|
||||
out = append(out, l)
|
||||
}
|
||||
if isDeb && enabled && kept {
|
||||
b.WriteString(strings.Join(out, "\n"))
|
||||
b.WriteString("\n\n")
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
Reference in New Issue
Block a user