diff --git a/cmd/prebuild/build.go b/cmd/prebuild/build.go new file mode 100644 index 000000000..49a0cb0d2 --- /dev/null +++ b/cmd/prebuild/build.go @@ -0,0 +1,66 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2023 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package main + +import ( + "regexp" + "strings" + + "github.com/roddhjav/apparmor.d/pkg/aa" + "github.com/roddhjav/apparmor.d/pkg/util" +) + +var ( + regABI = regexp.MustCompile(`abi.*,\n`) + regAttachments = regexp.MustCompile(`(profile .* @{exec_path})`) + regFlag = regexp.MustCompile(`flags=\(([^)]+)\)`) + regProfileHeader = regexp.MustCompile(` {`) +) + +// Set complain flag on all profiles +func BuildComplain(profile string) string { + if !Complain { + return profile + } + + flags := []string{} + matches := regFlag.FindStringSubmatch(profile) + if len(matches) != 0 { + flags = strings.Split(matches[1], ",") + if util.InSlice("complain", flags) { + return profile + } + } + flags = append(flags, "complain") + strFlags := " flags=(" + strings.Join(flags, ",") + ") {" + + // Remove all flags definition, then set manifest' flags + profile = regFlag.ReplaceAllLiteralString(profile, "") + return regProfileHeader.ReplaceAllLiteralString(profile, strFlags) +} + +// Bypass userspace tools restriction +func BuildUserspace(profile string) string { + p := aa.NewAppArmorProfile(profile) + p.ParseVariables() + p.ResolveAttachments() + att := p.NestAttachments() + matches := regAttachments.FindAllString(profile, -1) + if len(matches) > 0 { + strheader := strings.Replace(matches[0], "@{exec_path}", att, -1) + return regAttachments.ReplaceAllLiteralString(profile, strheader) + } + return profile +} + +// Remove abi header for distributions that don't support it +func BuildABI(profile string) string { + switch Distribution { + case "debian", "whonix": + return regABI.ReplaceAllLiteralString(profile, "") + default: + return profile + } +} diff --git a/cmd/prebuild/main.go b/cmd/prebuild/main.go new file mode 100644 index 000000000..83a36c9eb --- /dev/null +++ b/cmd/prebuild/main.go @@ -0,0 +1,112 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2023 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/arduino/go-paths-helper" + "github.com/roddhjav/apparmor.d/pkg/logging" + "github.com/roddhjav/apparmor.d/pkg/util" +) + +const usage = `prebuild [-h] [--full] [--complain] + + Internal tool to prebuild apparmor.d profiles for a given distribution. + +Options: + -h, --help Show this help message and exit. + -d, --dist The target Linux distribution. + -f, --full Set AppArmor for full system policy. + -c, --complain Set complain flag on all profiles. +` + +var ( + help bool + Full bool + Complain bool + Distribution string + Root *paths.Path + RootApparmord *paths.Path + + // Prepare the build directory with the following tasks + prepare = []prepareFunc{Synchronise, Ignore, Merge, Configure, SetFlags, SetFullSystemPolicy} + + // Build the profiles with the following build tasks + build = []buildFunc{BuildUserspace, BuildComplain, BuildABI} +) + +type prepareFunc func() error +type buildFunc func(string) string + +func init() { + Root = paths.New(".build") + RootApparmord = Root.Join("apparmor.d") + Distribution, _ = util.GetSupportedDistribution() + flag.BoolVar(&help, "h", false, "Show this help message and exit.") + flag.BoolVar(&help, "help", false, "Show this help message and exit.") + flag.BoolVar(&Full, "f", false, "Set AppArmor for full system policy.") + flag.BoolVar(&Full, "full", false, "Set AppArmor for full system policy.") + flag.BoolVar(&Complain, "c", false, "Set complain flag on all profiles.") + flag.BoolVar(&Complain, "complain", false, "Set complain flag on all profiles.") +} + +// Build the profiles. +func buildProfiles() error { + files, _ := RootApparmord.ReadDir(paths.FilterOutDirectories()) + for _, file := range files { + if !file.Exist() { + continue + } + content, _ := file.ReadFile() + profile := string(content) + for _, fct := range build { + profile = fct(profile) + } + if err := file.WriteFile([]byte(profile)); err != nil { + panic(err) + } + } + return nil +} + +func aaPrebuild() error { + logging.Step("Building apparmor.d profiles for %s.", Distribution) + + for _, fct := range prepare { + if err := fct(); err != nil { + return err + } + } + + if err := buildProfiles(); err != nil { + return err + } + logging.Success("Builded profiles with: ") + logging.Bullet("Bypass userspace tools restriction") + if Complain { + logging.Bullet("Set complain flag on all profiles") + } + switch Distribution { + case "debian", "whonix": + logging.Bullet("%s does not support abi 3.0 yet", Distribution) + } + return nil +} + +func main() { + flag.Usage = func() { fmt.Print(usage) } + flag.Parse() + if help { + flag.Usage() + os.Exit(0) + } + err := aaPrebuild() + if err != nil { + logging.Fatal(err.Error()) + } +} diff --git a/cmd/prebuild/main_test.go b/cmd/prebuild/main_test.go new file mode 100644 index 000000000..a42dfadb1 --- /dev/null +++ b/cmd/prebuild/main_test.go @@ -0,0 +1,80 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2023 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package main + +import ( + "os" + "os/exec" + "testing" +) + +func chdirGitRoot() { + cmd := exec.Command("git", "rev-parse", "--show-toplevel") + out, err := cmd.Output() + if err != nil { + panic(err) + } + root := string(out)[0 : len(out)-1] + if err := os.Chdir(root); err != nil { + panic(err) + } +} + +func Test_aaPrebuild(t *testing.T) { + tests := []struct { + name string + wantErr bool + full bool + complain bool + dist string + }{ + { + name: "Build for Archlinux", + wantErr: false, + full: false, + complain: true, + dist: "arch", + }, + { + name: "Build for Ubuntu", + wantErr: false, + full: true, + complain: false, + dist: "ubuntu", + }, + { + name: "Build for Debian", + wantErr: false, + full: true, + complain: false, + dist: "debian", + }, + { + name: "Build for OpenSUSE Tumbleweed", + wantErr: false, + full: true, + complain: true, + dist: "opensuse-tumbleweed", + }, + { + name: "Build for Fedora", + wantErr: true, + full: false, + complain: false, + dist: "fedora", + }, + } + chdirGitRoot() + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + Distribution = tt.dist + Complain = tt.complain + Full = tt.full + if err := aaPrebuild(); (err != nil) != tt.wantErr { + t.Errorf("aaPrebuild() error = %v, wantErr %v", err, tt.wantErr) + } + }) + } +} diff --git a/cmd/prebuild/prepare.go b/cmd/prebuild/prepare.go new file mode 100644 index 000000000..ef148a178 --- /dev/null +++ b/cmd/prebuild/prepare.go @@ -0,0 +1,215 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2023 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package main + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/arduino/go-paths-helper" + "github.com/roddhjav/apparmor.d/pkg/aa" + "github.com/roddhjav/apparmor.d/pkg/logging" +) + +// Initialize a new clean apparmor.d build directory +func Synchronise() error { + dirs := paths.PathList{RootApparmord, Root.Join("root")} + for _, dir := range dirs { + if err := dir.RemoveAll(); err != nil { + return err + } + } + for _, path := range []string{"./apparmor.d", "./root"} { + cmd := exec.Command("rsync", "-a", path, Root.String()) + if err := cmd.Run(); err != nil { + return err + } + } + logging.Success("Initialize a new clean apparmor.d build directory") + return nil +} + +// Ignore profiles and files as defined in dists/ignore/ +func Ignore() error { + for _, name := range []string{"main.ignore", Distribution + ".ignore"} { + path := paths.New("dists/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 err + } + for _, path := range files { + if err := path.RemoveAll(); err != nil { + return err + } + } + } else { + if err := profile.RemoveAll(); err != nil { + return err + } + } + } + logging.Success("Ignore profiles/files in %s", path) + } + return nil +} + +// Merge all profiles in a new apparmor.d directory +func Merge() error { + var 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 err + } + for _, file := range files { + err := os.Rename(file, RootApparmord.Join(filepath.Base(file)).String()) + if err != nil { + return err + } + } + + files, err = filepath.Glob(RootApparmord.Join(dirRemoved).String()) + if err != nil { + return err + } + for _, file := range files { + if err := paths.New(file).RemoveAll(); err != nil { + return err + } + } + idx = idx + 2 + } + logging.Success("Merge all profiles") + return nil +} + +// Set the distribution specificities +func Configure() error { + switch Distribution { + case "arch": + aa.Tunables["libexec"] = []string{"/{usr/,}lib"} + if err := setLibexec(); err != nil { + return err + } + + case "ubuntu", "opensuse": + aa.Tunables["libexec"] = []string{"/{usr/,}libexec"} + if err := setLibexec(); err != nil { + return err + } + + case "debian", "whonix": + aa.Tunables["libexec"] = []string{"/{usr/,}libexec"} + if err := setLibexec(); err != nil { + return err + } + + for _, dirname := range []string{"abstractions", "tunables"} { + files, err := filepath.Glob("dists/debian/" + dirname + "/*") + if err != nil { + return err + } + for _, file := range files { + path := paths.New(file) + if err := path.CopyTo(RootApparmord.Join(dirname, path.Base())); err != nil { + return err + } + } + } + + default: + return fmt.Errorf("%s is not a supported distribution", Distribution) + + } + return nil +} + +func setLibexec() error { + file, err := RootApparmord.Join("tunables", "etc.d", "apparmor.d").Append() + if err != nil { + return err + } + defer file.Close() + _, err = file.WriteString(`@{libexec}=` + aa.Tunables["libexec"][0]) + return err +} + +// Set flags on some profiles according to manifest defined in `dists/flags/` +func SetFlags() error { + for _, name := range []string{"main.flags", Distribution + ".flags"} { + path := paths.New("dists/flags/" + 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 err + } + + // Remove all flags definition, then set manifest' flags + res := regFlag.ReplaceAllLiteralString(string(content), "") + res = regProfileHeader.ReplaceAllLiteralString(res, flags) + if err := file.WriteFile([]byte(res)); err != nil { + return err + } + } + } + logging.Success("Set profile flags from %s", path) + } + return nil +} + +// Set AppArmor for full system policy +// See https://gitlab.com/apparmor/apparmor/-/wikis/FullSystemPolicy +// https://gitlab.com/apparmor/apparmor/-/wikis/AppArmorInSystemd#early-policy-loads +func SetFullSystemPolicy() error { + if !Full { + return nil + } + + for _, name := range []string{"init", "systemd"} { + err := paths.New("apparmor.d/groups/_full/" + name).CopyTo(RootApparmord.Join(name)) + if err != nil { + return err + } + } + logging.Success("Configure AppArmor for full system policy") + return nil +} diff --git a/pkg/aa/profile.go b/pkg/aa/profile.go new file mode 100644 index 000000000..69f3e844d --- /dev/null +++ b/pkg/aa/profile.go @@ -0,0 +1,107 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2023 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +// Warning: this is purposely not using a Yacc parser. Its only aim is to +// extract variables and attachments for apparmor.d profile + +package aa + +import ( + "regexp" + "strings" + + "golang.org/x/exp/maps" +) + +var ( + regVariablesDef = regexp.MustCompile(`@{(.*)}\s*[+=]+\s*(.*)`) + regVariablesRef = regexp.MustCompile(`@{([^{}]+)}`) + + // Tunables + Tunables = map[string][]string{ + "libexec": {}, + "multiarch": {"*-linux-gnu*"}, + "user_share_dirs": {"/home/*/.local/share"}, + "etc_ro": {"/{usr/,}etc/"}, + } +) + +type AppArmorProfile struct { + Content string + Variables map[string][]string + Attachments []string +} + +func NewAppArmorProfile(content string) *AppArmorProfile { + variables := make(map[string][]string) + maps.Copy(variables, Tunables) + return &AppArmorProfile{ + Content: content, + Variables: variables, + Attachments: []string{}, + } +} + +// ParseVariables extract all variables from the profile +func (p *AppArmorProfile) ParseVariables() { + matches := regVariablesDef.FindAllStringSubmatch(p.Content, -1) + for _, match := range matches { + if len(match) > 2 { + key := match[1] + values := match[2] + if _, ok := p.Variables[key]; ok { + p.Variables[key] = append(p.Variables[key], strings.Split(values, " ")...) + } else { + p.Variables[key] = strings.Split(values, " ") + } + } + } +} + +// resolve recursively resolves all variables references +func (p *AppArmorProfile) resolve(str string) []string { + if strings.Contains(str, "@{") { + vars := []string{} + match := regVariablesRef.FindStringSubmatch(str) + if len(match) > 1 { + variable := match[0] + varname := match[1] + if len(p.Variables[varname]) > 1 { + for _, value := range p.Variables[varname] { + newVar := strings.ReplaceAll(str, variable, value) + vars = append(vars, p.resolve(newVar)...) + } + } else { + newVar := strings.ReplaceAll(str, variable, p.Variables[varname][0]) + vars = append(vars, p.resolve(newVar)...) + } + } else { + vars = append(vars, str) + } + return vars + } + return []string{str} +} + +// ResolveAttachments resolve profile attachments defined in exec_path +func (p *AppArmorProfile) ResolveAttachments() { + for _, exec := range p.Variables["exec_path"] { + p.Attachments = append(p.Attachments, p.resolve(exec)...) + } +} + +// NestAttachments return a nested attachment string +func (p *AppArmorProfile) NestAttachments() string { + if len(p.Attachments) == 0 { + return "" + } else if len(p.Attachments) == 1 { + return p.Attachments[0] + } else { + res := []string{} + for _, attachment := range p.Attachments { + res = append(res, attachment[1:]) + } + return "/{" + strings.Join(res, ",") + "}" + } +} diff --git a/pkg/aa/profile_test.go b/pkg/aa/profile_test.go new file mode 100644 index 000000000..7524bb52d --- /dev/null +++ b/pkg/aa/profile_test.go @@ -0,0 +1,201 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2023 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package aa + +import ( + "reflect" + "testing" +) + +func TestNewAppArmorProfile(t *testing.T) { + tests := []struct { + name string + content string + want *AppArmorProfile + }{ + { + name: "aa", + content: "", + want: &AppArmorProfile{ + Content: "", + Variables: map[string][]string{ + "libexec": {}, + "etc_ro": {"/{usr/,}etc/"}, + "multiarch": {"*-linux-gnu*"}, + "user_share_dirs": {"/home/*/.local/share"}, + }, + Attachments: []string{}, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := NewAppArmorProfile(tt.content); !reflect.DeepEqual(got, tt.want) { + t.Errorf("NewAppArmorProfile() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestAppArmorProfile_ParseVariables(t *testing.T) { + tests := []struct { + name string + content string + want map[string][]string + }{ + { + name: "firefox", + content: `@{firefox_name} = firefox{,-esr,-bin} + @{firefox_lib_dirs} = /{usr/,}lib{,32,64}/@{firefox_name} /opt/@{firefox_name} + @{firefox_config_dirs} = @{HOME}/.mozilla/ + @{firefox_cache_dirs} = @{user_cache_dirs}/mozilla/ + @{exec_path} = /{usr/,}bin/@{firefox_name} @{firefox_lib_dirs}/@{firefox_name} + `, + want: map[string][]string{ + "firefox_name": {"firefox{,-esr,-bin}"}, + "firefox_config_dirs": {"@{HOME}/.mozilla/"}, + "firefox_lib_dirs": {"/{usr/,}lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}, + "firefox_cache_dirs": {"@{user_cache_dirs}/mozilla/"}, + "exec_path": {"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}, + }, + }, + { + name: "xorg", + content: `@{exec_path} = /{usr/,}bin/X + @{exec_path} += /{usr/,}bin/Xorg{,.bin} + @{exec_path} += /{usr/,}lib/Xorg{,.wrap} + @{exec_path} += /{usr/,}lib/xorg/Xorg{,.wrap}`, + want: map[string][]string{ + "exec_path": { + "/{usr/,}bin/X", + "/{usr/,}bin/Xorg{,.bin}", + "/{usr/,}lib/Xorg{,.wrap}", + "/{usr/,}lib/xorg/Xorg{,.wrap}", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &AppArmorProfile{ + Content: tt.content, + Variables: map[string][]string{}, + Attachments: []string{}, + } + + p.ParseVariables() + if !reflect.DeepEqual(p.Variables, tt.want) { + t.Errorf("AppArmorProfile.ParseVariables() = %v, want %v", p.Variables, tt.want) + } + }) + } +} + +func TestAppArmorProfile_ResolveAttachments(t *testing.T) { + tests := []struct { + name string + variables map[string][]string + want []string + }{ + { + name: "firefox", + variables: map[string][]string{ + "firefox_name": {"firefox{,-esr,-bin}"}, + "firefox_lib_dirs": {"/{usr/,}/lib{,32,64}/@{firefox_name}", "/opt/@{firefox_name}"}, + "exec_path": {"/{usr/,}bin/@{firefox_name}", "@{firefox_lib_dirs}/@{firefox_name}"}, + }, + want: []string{ + "/{usr/,}bin/firefox{,-esr,-bin}", + "/{usr/,}/lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}", + "/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}", + }, + }, + { + name: "chromium", + variables: map[string][]string{ + "chromium_name": {"chromium"}, + "chromium_lib_dirs": {"/{usr/,}lib/@{chromium_name}"}, + "exec_path": {"@{chromium_lib_dirs}/@{chromium_name}"}, + }, + want: []string{ + "/{usr/,}lib/chromium/chromium", + }, + }, + { + name: "geoclue", + variables: map[string][]string{ + "libexec": {"/{usr/,}libexec"}, + "exec_path": {"@{libexec}/geoclue", "@{libexec}/geoclue-2.0/demos/agent"}, + }, + want: []string{ + "/{usr/,}libexec/geoclue", + "/{usr/,}libexec/geoclue-2.0/demos/agent", + }, + }, + { + name: "opera", + variables: map[string][]string{ + "multiarch": {"*-linux-gnu*"}, + "chromium_name": {"opera{,-beta,-developer}"}, + "chromium_lib_dirs": {"/{usr/,}lib/@{multiarch}/@{chromium_name}"}, + "exec_path": {"@{chromium_lib_dirs}/@{chromium_name}"}, + }, + want: []string{ + "/{usr/,}lib/*-linux-gnu*/opera{,-beta,-developer}/opera{,-beta,-developer}", + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &AppArmorProfile{ + Content: "", + Variables: tt.variables, + Attachments: []string{}, + } + p.ResolveAttachments() + if !reflect.DeepEqual(p.Attachments, tt.want) { + t.Errorf("AppArmorProfile.ResolveAttachments() = %v, want %v", p.Attachments, tt.want) + } + }) + } +} + +func TestAppArmorProfile_NestAttachments(t *testing.T) { + tests := []struct { + name string + Attachments []string + want string + }{ + { + name: "firefox", + Attachments: []string{ + "/{usr/,}bin/firefox{,-esr,-bin}", + "/{usr/,}lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin}", + "/opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}", + }, + want: "/{{usr/,}bin/firefox{,-esr,-bin},{usr/,}lib{,32,64}/firefox{,-esr,-bin}/firefox{,-esr,-bin},opt/firefox{,-esr,-bin}/firefox{,-esr,-bin}}", + }, + { + name: "geoclue", + Attachments: []string{ + "/{usr/,}libexec/geoclue", + "/{usr/,}libexec/geoclue-2.0/demos/agent", + }, + want: "/{{usr/,}libexec/geoclue,{usr/,}libexec/geoclue-2.0/demos/agent}", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + p := &AppArmorProfile{ + Content: "", + Variables: map[string][]string{}, + Attachments: tt.Attachments, + } + if got := p.NestAttachments(); got != tt.want { + t.Errorf("AppArmorProfile.NestAttachments() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/util/os.go b/pkg/util/os.go new file mode 100644 index 000000000..0e12ab6c9 --- /dev/null +++ b/pkg/util/os.go @@ -0,0 +1,47 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2023 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package util + +import ( + "fmt" + "os" + "strings" + + "github.com/arduino/go-paths-helper" +) + +var osReleaseFile = "/etc/os-release" + +var firstPartyDists = []string{"arch", "debian", "ubuntu", "opensuse", "whonix"} + +func GetSupportedDistribution() (string, error) { + dist, present := os.LookupEnv("DISTRIBUTION") + if present { + return dist, nil + } + + lines, err := paths.New(osReleaseFile).ReadFileAsLines() + if err != nil { + return "", err + } + + id := "" + id_like := "" + for _, line := range lines { + item := strings.Split(line, "=") + if item[0] == "ID" { + id = strings.Split(strings.Trim(item[1], "\""), " ")[0] + } else if item[0] == "ID_LIKE" { + id_like = strings.Split(strings.Trim(item[1], "\""), " ")[0] + } + } + + if InSlice(id, firstPartyDists) { + return id, nil + } else if InSlice(id_like, firstPartyDists) { + return id_like, nil + } + return id, fmt.Errorf("%s is not a supported distribution", id) +} diff --git a/pkg/util/os_test.go b/pkg/util/os_test.go new file mode 100644 index 000000000..82cec0901 --- /dev/null +++ b/pkg/util/os_test.go @@ -0,0 +1,137 @@ +// apparmor.d - Full set of apparmor profiles +// Copyright (C) 2023 Alexandre Pujol +// SPDX-License-Identifier: GPL-2.0-only + +package util + +import ( + "testing" + + "github.com/arduino/go-paths-helper" +) + +const ( + Archlinux = `NAME="Arch Linux" +PRETTY_NAME="Arch Linux" +ID=arch +BUILD_ID=rolling +ANSI_COLOR="38;2;23;147;209" +HOME_URL="https://archlinux.org/" +DOCUMENTATION_URL="https://wiki.archlinux.org/" +SUPPORT_URL="https://bbs.archlinux.org/" +BUG_REPORT_URL="https://bugs.archlinux.org/" +PRIVACY_POLICY_URL="https://terms.archlinux.org/docs/privacy-policy/" +LOGO=archlinux-logo` + + Ubuntu = `PRETTY_NAME="Ubuntu 22.04.2 LTS" +NAME="Ubuntu" +VERSION_ID="22.04" +VERSION="22.04.2 LTS (Jammy Jellyfish)" +VERSION_CODENAME=jammy +ID=ubuntu +ID_LIKE=debian +HOME_URL="https://www.ubuntu.com/" +SUPPORT_URL="https://help.ubuntu.com/" +BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/" +PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy" +UBUNTU_CODENAME=jammy` + + Debian = `PRETTY_NAME="Debian GNU/Linux 11 (bullseye)" +NAME="Debian GNU/Linux" +VERSION_ID="11" +VERSION="11 (bullseye)" +VERSION_CODENAME=bullseye +ID=debian +HOME_URL="https://www.debian.org/" +SUPPORT_URL="https://www.debian.org/support" +BUG_REPORT_URL="https://bugs.debian.org/"` + + OpenSUSETumbleweed = `ID="opensuse-tumbleweed" +ID_LIKE="opensuse suse" +VERSION_ID="20230404" +PRETTY_NAME="openSUSE Tumbleweed" +ANSI_COLOR="0;32" +CPE_NAME="cpe:/o:opensuse:tumbleweed:20230404" +BUG_REPORT_URL="https://bugs.opensuse.org" +HOME_URL="https://www.opensuse.org/" +DOCUMENTATION_URL="https://en.opensuse.org/Portal:Tumbleweed" +LOGO="distributor-logo-Tumbleweed"` + + ArcoLinux = `NAME=ArcoLinux +ID=arcolinux +ID_LIKE=arch +BUILD_ID=rolling +ANSI_COLOR="0;36" +HOME_URL="https://arcolinux.info/" +SUPPORT_URL="https://arcolinuxforum.com/" +BUG_REPORT_URL="https://github.com/arcolinux" +LOGO=arcolinux-hello` + + Fedora = `NAME="Fedora Linux" +VERSION="37 (Workstation Edition)" +ID=fedora +VERSION_ID=37 +VERSION_CODENAME="" +PLATFORM_ID="platform:f37" +PRETTY_NAME="Fedora Linux 37 (Workstation Edition)" +ANSI_COLOR="0;38;2;60;110;180" +LOGO=fedora-logo-icon` +) + +func TestGetSupportedDistribution(t *testing.T) { + tests := []struct { + name string + osRelease string + want string + wantErr bool + }{ + { + name: "Archlinux", + osRelease: Archlinux, + want: "arch", + wantErr: false, + }, + { + name: "Ubuntu", + osRelease: Ubuntu, + want: "ubuntu", + wantErr: false, + }, + { + name: "Debian", + osRelease: Debian, + want: "debian", + wantErr: false, + }, + { + name: "OpenSUSE Tumbleweed", + osRelease: OpenSUSETumbleweed, + want: "opensuse", + wantErr: false, + }, + { + name: "Fedora", + osRelease: Fedora, + want: "fedora", + wantErr: true, + }, + } + + osReleaseFile = "os-release" + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := paths.New(osReleaseFile).WriteFile([]byte(tt.osRelease)) + if err != nil { + return + } + got, err := GetSupportedDistribution() + if (err != nil) != tt.wantErr { + t.Errorf("ReadLinuxDistribution() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("ReadLinuxDistribution() = %v, want %v", got, tt.want) + } + }) + } +}