diff --git a/pkg/prebuild/prepare.go b/pkg/prebuild/prepare.go new file mode 100644 index 00000000..0b005802 --- /dev/null +++ b/pkg/prebuild/prepare.go @@ -0,0 +1,249 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2023-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package prebuild + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/arduino/go-paths-helper" + "github.com/roddhjav/apparmor.d/pkg/logging" + "github.com/roddhjav/apparmor.d/pkg/util" +) + +// Prepare the build directory with the following tasks +var ( + Prepares = []PrepareFunc{ + Synchronise, + Ignore, + Merge, + Configure, + SetFlags, + SetProfileSystemd, + } + PrepareMsg = map[string]string{ + "Synchronise": "Initialize a new clean apparmor.d build directory", + "Ignore": "Ignore profiles and files from:", + "Merge": "Merge all profiles", + "Configure": "Set distribution specificities", + "SetFlags": "Set flags on some profiles", + "SetProfileSystemd": "Use the systemd unit file to set a profile for a given unit", + "SetEarlySystemd": "Set systemd unit drop in files to ensure some service start after apparmor", + "SetFullSystemPolicy": "Configure AppArmor for full system policy", + } +) + +type PrepareFunc func() ([]string, error) + +// Initialize a new clean apparmor.d build directory +func Synchronise() ([]string, error) { + res := []string{} + dirs := paths.PathList{RootApparmord, Root.Join("root"), Root.Join("systemd")} + for _, dir := range dirs { + if err := dir.RemoveAll(); err != nil { + return res, err + } + } + for _, name := range []string{"apparmor.d", "root"} { + if err := copyTo(paths.New(name), Root.Join(name)); err != nil { + return res, err + } + } + return res, nil +} + +// Ignore profiles and files as defined in dists/ignore/ +func Ignore() ([]string, error) { + res := []string{} + for _, name := range []string{"main.ignore", Distribution + ".ignore"} { + path := DistDir.Join("ignore", name) + if !path.Exist() { + continue + } + lines, _ := path.ReadFileAsLines() + for _, line := range lines { + if strings.HasPrefix(line, "#") || line == "" { + continue + } + profile := Root.Join(line) + if profile.NotExist() { + files, err := RootApparmord.ReadDirRecursiveFiltered(nil, paths.FilterNames(line)) + if err != nil { + return res, err + } + for _, path := range files { + if err := path.RemoveAll(); err != nil { + return res, err + } + } + } else { + if err := profile.RemoveAll(); err != nil { + return res, err + } + } + } + res = append(res, path.String()) + } + return res, nil +} + +// Merge all profiles in a new apparmor.d directory +func Merge() ([]string, error) { + res := []string{} + dirToMerge := []string{ + "groups/*/*", "groups", + "profiles-*-*/*", "profiles-*", + } + + idx := 0 + for idx < len(dirToMerge)-1 { + dirMoved, dirRemoved := dirToMerge[idx], dirToMerge[idx+1] + files, err := filepath.Glob(RootApparmord.Join(dirMoved).String()) + if err != nil { + return res, err + } + for _, file := range files { + err := os.Rename(file, RootApparmord.Join(filepath.Base(file)).String()) + if err != nil { + return res, err + } + } + + files, err = filepath.Glob(RootApparmord.Join(dirRemoved).String()) + if err != nil { + return []string{}, err + } + for _, file := range files { + if err := paths.New(file).RemoveAll(); err != nil { + return res, err + } + } + idx = idx + 2 + } + return res, nil +} + +// Set the distribution specificities +func Configure() ([]string, error) { + res := []string{} + switch Distribution { + case "arch", "opensuse": + + case "ubuntu", "neon": + debianOverwriteClean() + if overwrite { + profiles := getOverwriteProfiles() + debianOverwrite(profiles) + } else { + if err := copyTo(DistDir.Join("ubuntu"), RootApparmord); err != nil { + return res, err + } + } + case "debian", "whonix": + debianOverwriteClean() + + // Copy Debian specific abstractions + if err := copyTo(DistDir.Join("ubuntu"), RootApparmord); err != nil { + return res, err + } + + default: + return []string{}, fmt.Errorf("%s is not a supported distribution", Distribution) + + } + return res, nil +} + +// Set flags on some profiles according to manifest defined in `dists/flags/` +func SetFlags() ([]string, error) { + res := []string{} + for _, name := range []string{"main.flags", Distribution + ".flags"} { + path := FlagDir.Join(name) + if !path.Exist() { + continue + } + lines, _ := path.ReadFileAsLines() + for _, line := range lines { + if strings.HasPrefix(line, "#") || line == "" { + continue + } + manifest := strings.Split(line, " ") + profile := manifest[0] + file := RootApparmord.Join(profile) + if !file.Exist() { + logging.Warning("Profile %s not found", profile) + continue + } + + // If flags is set, overwrite profile flag + if len(manifest) > 1 { + flags := " flags=(" + manifest[1] + ") {" + content, err := file.ReadFile() + if err != nil { + return res, err + } + + // Remove all flags definition, then set manifest' flags + out := regFlags.ReplaceAllLiteralString(string(content), "") + out = regProfileHeader.ReplaceAllLiteralString(out, flags) + if err := file.WriteFile([]byte(out)); err != nil { + return res, err + } + } + } + res = append(res, path.String()) + } + return res, nil +} + +// Use the systemd unit file to set a profile for a given unit +func SetProfileSystemd() ([]string, error) { + return []string{}, copyTo(paths.New("systemd/default/"), Root.Join("systemd")) +} + +// Set systemd unit drop in files to ensure some service start after apparmor +func SetEarlySystemd() ([]string, error) { + return []string{}, copyTo(paths.New("systemd/early/"), Root.Join("systemd")) +} + +// Set AppArmor for (experimental) full system policy. +// See https://apparmor.pujol.io/full-system-policy/ +func SetFullSystemPolicy() ([]string, error) { + res := []string{} + // Install full system policy profiles + if err := copyTo(paths.New("apparmor.d/groups/_full/"), Root.Join("apparmor.d")); err != nil { + return res, err + } + + // Set systemd profile name + path := RootApparmord.Join("tunables/multiarch.d/system") + content, err := path.ReadFile() + if err != nil { + return res, err + } + out := strings.Replace(string(content), "@{systemd}=unconfined", "@{systemd}=systemd", -1) + out = strings.Replace(out, "@{systemd_user}=unconfined", "@{systemd_user}=systemd-user", -1) + if err := path.WriteFile([]byte(out)); err != nil { + return res, err + } + + // Fix conflicting x modifiers in abstractions - FIXME: Temporary solution + path = RootApparmord.Join("abstractions/gstreamer") + content, err = path.ReadFile() + if err != nil { + return res, err + } + out = string(content) + regFixConflictX := util.ToRegexRepl([]string{`.*gst-plugin-scanner.*`, ``}) + out = regFixConflictX.Replace(out) + if err := path.WriteFile([]byte(out)); err != nil { + return res, err + } + + // Set systemd unit drop-in files + return res, copyTo(paths.New("systemd/full/"), Root.Join("systemd")) +} diff --git a/pkg/prebuild/tools.go b/pkg/prebuild/tools.go new file mode 100644 index 00000000..5550e73f --- /dev/null +++ b/pkg/prebuild/tools.go @@ -0,0 +1,145 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2023-2024 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package prebuild + +import ( + "os" + "strings" + + "github.com/arduino/go-paths-helper" + "golang.org/x/exp/slices" +) + +var ( + osReleaseFile = "/etc/os-release" + supportedDists = map[string][]string{ + "arch": {}, + "debian": {}, + "ubuntu": {"ubuntu", "neon"}, + "opensuse": {"suse", "opensuse-tumbleweed"}, + "whonix": {}, + } +) + +func NewOSRelease() map[string]string { + var lines []string + var err error + for _, name := range []string{osReleaseFile, "/usr/lib/os-release"} { + path := paths.New(name) + if path.Exist() { + lines, err = path.ReadFileAsLines() + if err != nil { + panic(err) + } + break + } + } + os := map[string]string{} + for _, line := range lines { + item := strings.Split(line, "=") + if len(item) == 2 { + os[item[0]] = strings.Trim(item[1], "\"") + } + } + return os +} + +func getSupportedDistribution() string { + dist, present := os.LookupEnv("DISTRIBUTION") + if present { + return dist + } + + os := NewOSRelease() + id := os["ID"] + if id == "ubuntu" { + return id + } + if id == "neon" { + return "ubuntu" + } + id_like := os["ID_LIKE"] + for main, based := range supportedDists { + if main == id || main == id_like { + return main + } else if slices.Contains(based, id) { + return main + } else if slices.Contains(based, id_like) { + return main + } + } + return id +} + +func copyTo(src *paths.Path, dst *paths.Path) error { + files, err := src.ReadDirRecursiveFiltered(nil, paths.FilterOutDirectories(), paths.FilterOutNames("README.md")) + if err != nil { + return err + } + for _, file := range files { + destination, err := file.RelFrom(src) + if err != nil { + return err + } + destination = dst.JoinPath(destination) + if err := destination.Parent().MkdirAll(); err != nil { + return err + } + if err := file.CopyTo(destination); err != nil { + return err + } + } + return nil +} + +// Overwrite upstream profile: rename our profile & hide upstream +func debianOverwrite(files []string) { + const ext = ".apparmor.d" + file, err := paths.New("debian/apparmor.d.hide").Append() + if err != nil { + panic(err) + } + for _, name := range files { + origin := RootApparmord.Join(name) + dest := RootApparmord.Join(name + ext) + if err := origin.Rename(dest); err != nil { + panic(err) + } + if _, err := file.WriteString("/etc/apparmor.d/" + name + "\n"); err != nil { + panic(err) + } + } +} + +// Clean the debian/apparmor.d.hide file +func debianOverwriteClean() { + const debianHide = `# This file is generated by "make", all edit will be lost. + +/etc/apparmor.d/usr.bin.firefox +/etc/apparmor.d/usr.sbin.cups-browsed +/etc/apparmor.d/usr.sbin.cupsd +/etc/apparmor.d/usr.sbin.rsyslogd +` + path := paths.New("debian/apparmor.d.hide") + if err := path.WriteFile([]byte(debianHide)); err != nil { + panic(err) + } +} + +// Get the list of upstream profiles to overwrite from dist/overwrite +func getOverwriteProfiles() []string { + res := []string{} + lines, err := DistDir.Join("overwrite").ReadFileAsLines() + if err != nil { + panic(err) + } + for _, line := range lines { + if strings.HasPrefix(line, "#") || line == "" { + continue + } + res = append(res, line) + } + return res +}