diff --git a/internal/os/feature_detection.go b/internal/os/feature_detection.go index c23b8b67b..1aaf3f23c 100644 --- a/internal/os/feature_detection.go +++ b/internal/os/feature_detection.go @@ -2,6 +2,8 @@ package os import ( "context" + "encoding/json" + "errors" "fmt" "regexp" @@ -10,6 +12,7 @@ import ( "github.com/anchore/syft/internal/sbomsync" "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" ) @@ -19,8 +22,11 @@ func DetectFeatures(_ context.Context, resolver file.Resolver, builder sbomsync. return } - err := findRhelFeatures(resolver, s.Artifacts.LinuxDistribution) - if err != nil { + if err := findRhelFeatures(resolver, s.Artifacts.LinuxDistribution); err != nil { + log.WithFields("error", err, "release", s.Artifacts.LinuxDistribution).Trace("error searching for extended support") + } + + if err := findUbuntuFeatures(resolver, s.Artifacts.LinuxDistribution, s.Artifacts.Packages); err != nil { log.WithFields("error", err, "release", s.Artifacts.LinuxDistribution).Trace("error searching for extended support") } }) @@ -28,6 +34,9 @@ func DetectFeatures(_ context.Context, resolver file.Resolver, builder sbomsync. return nil } +// rhelEUSPatterns match a RHEL content manifest referencing an extended-update-support (EUS) repo. +var rhelEUSPatterns = []*regexp.Regexp{regexp.MustCompile(`baseos-eus`), regexp.MustCompile(`baseos-eus|appstream-eus`)} + func findRhelFeatures(resolver file.Resolver, release *linux.Release) error { if release == nil || release.ID != "rhel" { return nil @@ -37,9 +46,9 @@ func findRhelFeatures(resolver file.Resolver, release *linux.Release) error { return fmt.Errorf("unable to find content manifests: %w", err) } for _, contentManifestFile := range contentManifestFiles { - found, err := hasRhelExtendedSupportInContentManifest(resolver, contentManifestFile) + found, err := fileMatchesAny(resolver, contentManifestFile, rhelEUSPatterns...) if err != nil { - return fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.String(), err) + return fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.RealPath, err) } if found { release.ExtendedSupport = true @@ -49,13 +58,152 @@ func findRhelFeatures(resolver file.Resolver, release *linux.Release) error { return nil } -func hasRhelExtendedSupportInContentManifest(resolver file.Resolver, contentManifestFile file.Location) (bool, error) { - contents, err := resolver.FileContentsByLocation(contentManifestFile) - if err != nil { - return false, fmt.Errorf("unable to read content manifest from %s: %w", contentManifestFile.String(), err) - } - defer internal.CloseAndLogError(contents, "content-manifest") +// Ubuntu Pro (formerly Ubuntu Advantage) is Canonical's subscription that unlocks Expanded Security +// Maintenance (ESM): security fixes past a package's standard support window. we detect the two base ESM +// streams, esm-infra (the `main` repo) and esm-apps (`universe`), and collapse them into one ExtendedSupport +// signal; the infra-vs-apps split is disambiguated by the downstream consumer, not by this boolean. +// +// the other Pro streams on the same host (esm.ubuntu.com/fips, /fips-updates, /realtime) are deliberately +// excluded: they are separate compliance products with their own downstream channels, and a host can run +// them with base ESM disabled, so folding them in would be unsound. +// +// detection is deliberately forgiving: apt/status evidence disappears when ESM is disabled, but an installed +// +esmN package is durable proof ESM content is on disk. so ExtendedSupport means "ESM was or is in effect +// for this host or its content", not "currently entitled" (mere eligibility, without evidence, is not enough). - patterns := []*regexp.Regexp{regexp.MustCompile(`baseos-eus`), regexp.MustCompile(`baseos-eus|appstream-eus`)} +// esmAptSourcePattern matches an uncommented apt source (classic or DEB822) or auth entry pointing at the +// plain ESM streams esm.ubuntu.com/infra or /apps. it deliberately scopes to /(infra|apps) so the other +// Pro streams served from the same host (/fips, /fips-updates, /realtime) are never folded into the base +// esm channel. the [^#\n]* before the host ensures a leading `#` (comment) can never satisfy the match. +var esmAptSourcePattern = regexp.MustCompile(`(?m)^[^#\n]*esm\.ubuntu\.com/(infra|apps)`) + +// esmVersionPattern matches a dpkg version carrying a numbered ESM pocket suffix, e.g. `...+esm1` or +// `...~esm2`. the trailing \d scopes this to the ESM pocket and excludes other Pro pockets (e.g. +fips). +var esmVersionPattern = regexp.MustCompile(`[~+]esm\d`) + +func findUbuntuFeatures(resolver file.Resolver, release *linux.Release, packages *pkg.Collection) error { + if release == nil || release.ID != "ubuntu" { + return nil + } + + // an uncommented esm.ubuntu.com apt source or an active pro esm service both prove Pro/ESM was enabled. + // these are the durable, file-based signals present on attached hosts. a read error in one signal must + // not suppress the others, so errors are collected and detection continues to the next signal. + var errs error + for _, check := range []func() (bool, error){ + func() (bool, error) { return hasUbuntuESMAptEvidence(resolver) }, + func() (bool, error) { return hasActiveUbuntuESMService(resolver) }, + } { + found, err := check() + if err != nil { + errs = errors.Join(errs, err) + continue + } + if found { + release.ExtendedSupport = true + return errs + } + } + + // fallback for images (typically not pro-attached): an installed package whose version carries an + // +esm/~esm suffix was pulled from an ESM pocket, so ESM was in effect when it was installed. + if hasInstalledESMPackage(packages) { + release.ExtendedSupport = true + } + return errs +} + +// hasUbuntuESMAptEvidence reports whether any apt source (classic .list or DEB822 .sources) or auth entry +// references esm.ubuntu.com via an uncommented line. +func hasUbuntuESMAptEvidence(resolver file.Resolver) (bool, error) { + // apt source files live in a directory (classic .list or DEB822 .sources), the credentials file is a fixed path. + sourceLocations, err := resolver.FilesByGlob("/etc/apt/sources.list.d/*") + if err != nil { + return false, fmt.Errorf("unable to find apt esm sources: %w", err) + } + authLocations, err := resolver.FilesByPath("/etc/apt/auth.conf.d/90ubuntu-advantage") + if err != nil { + return false, fmt.Errorf("unable to find apt esm auth: %w", err) + } + + // ponytail: a DEB822 source with `Enabled: no` would still match; the pro client removes/comments the + // file on disable rather than toggling that key, so this is an accepted false-positive ceiling. the + // auth.conf.d entry is deleted by the pro client on disable, so it is a durable positive-only signal. + for _, location := range append(sourceLocations, authLocations...) { + match, err := fileMatchesAny(resolver, location, esmAptSourcePattern) + if err != nil { + return false, err + } + if match { + return true, nil + } + } + return false, nil +} + +// hasActiveUbuntuESMService reports whether the ubuntu-advantage/pro status cache shows an enabled esm service. +func hasActiveUbuntuESMService(resolver file.Resolver) (bool, error) { + locations, err := resolver.FilesByPath("/var/lib/ubuntu-advantage/status.json") + if err != nil { + return false, fmt.Errorf("unable to find ubuntu-advantage status: %w", err) + } + for _, location := range locations { + enabled, err := hasEnabledESMServiceInStatus(resolver, location) + if err != nil { + return false, err + } + if enabled { + return true, nil + } + } + return false, nil +} + +func hasEnabledESMServiceInStatus(resolver file.Resolver, location file.Location) (bool, error) { + contents, err := resolver.FileContentsByLocation(location) + if err != nil { + return false, fmt.Errorf("unable to read ubuntu-advantage status from %s: %w", location.RealPath, err) + } + defer internal.CloseAndLogError(contents, location.RealPath) + + var status struct { + Services []struct { + Name string `json:"name"` + Status string `json:"status"` + } `json:"services"` + } + if err := json.NewDecoder(contents).Decode(&status); err != nil { + return false, fmt.Errorf("unable to parse ubuntu-advantage status from %s: %w", location.RealPath, err) + } + + // pro status values are enabled/disabled/n-a/warning/—; only "enabled" proves the stream is active. + // warning/expired states are intentionally excluded here and left to the apt-source and installed-package signals. + for _, svc := range status.Services { + if (svc.Name == "esm-infra" || svc.Name == "esm-apps") && svc.Status == "enabled" { + return true, nil + } + } + return false, nil +} + +// hasInstalledESMPackage reports whether any installed dpkg package version carries an ESM pocket suffix. +func hasInstalledESMPackage(packages *pkg.Collection) bool { + if packages == nil { + return false + } + for p := range packages.Enumerate(pkg.DebPkg) { + if esmVersionPattern.MatchString(p.Version) { + return true + } + } + return false +} + +func fileMatchesAny(resolver file.Resolver, location file.Location, patterns ...*regexp.Regexp) (bool, error) { + contents, err := resolver.FileContentsByLocation(location) + if err != nil { + return false, fmt.Errorf("unable to read %s: %w", location.RealPath, err) + } + defer internal.CloseAndLogError(contents, location.RealPath) return internal.MatchAnyFromReader(contents, patterns...) } diff --git a/internal/os/feature_detection_test.go b/internal/os/feature_detection_test.go index 9a1fe32f9..bb8a30de1 100644 --- a/internal/os/feature_detection_test.go +++ b/internal/os/feature_detection_test.go @@ -2,6 +2,11 @@ package os_test import ( "context" + "io" + stdos "os" + "path" + "path/filepath" + "slices" "testing" "github.com/stretchr/testify/require" @@ -11,7 +16,9 @@ import ( "github.com/anchore/syft/internal/sbomsync" "github.com/anchore/syft/internal/task" "github.com/anchore/syft/syft" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" + "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" ) @@ -99,3 +106,210 @@ func Test_EnvironmentTask(t *testing.T) { }) } } + +func Test_findUbuntuFeatures(t *testing.T) { + tests := []struct { + name string + dir string + releaseID string + packages []pkg.Package + expected bool + }{ + { + name: "esm apt source present", + dir: "testdata/ubuntu_esm_apt", + releaseID: "ubuntu", + expected: true, + }, + { + name: "esm apt source in DEB822 .sources format", + dir: "testdata/ubuntu_esm_deb822", + releaseID: "ubuntu", + expected: true, + }, + { + name: "esm evidence in apt auth.conf.d", + dir: "testdata/ubuntu_esm_authconf", + releaseID: "ubuntu", + expected: true, + }, + { + name: "active esm service in ubuntu-advantage status", + dir: "testdata/ubuntu_esm_status", + releaseID: "ubuntu", + expected: true, + }, + { + name: "only esm-infra enabled (esm-apps disabled)", + dir: "testdata/ubuntu_esm_infra_only", + releaseID: "ubuntu", + expected: true, + }, + { + name: "malformed status.json does not panic or match", + dir: "testdata/ubuntu_esm_malformed", + releaseID: "ubuntu", + expected: false, + }, + { + name: "installed +esm package version", + dir: "testdata/ubuntu_plain", + releaseID: "ubuntu", + packages: []pkg.Package{ + {Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg}, + }, + expected: true, + }, + { + name: "installed ~esm package version", + dir: "testdata/ubuntu_plain", + releaseID: "ubuntu", + packages: []pkg.Package{ + {Name: "libcap2", Version: "1:2.32-1ubuntu0.1~esm1", Type: pkg.DebPkg}, + }, + expected: true, + }, + { + name: "plain ubuntu with no esm evidence", + dir: "testdata/ubuntu_plain", + releaseID: "ubuntu", + packages: []pkg.Package{ + {Name: "openssl", Version: "1.1.1f-1ubuntu2.19", Type: pkg.DebPkg}, + }, + expected: false, + }, + { + name: "commented esm source and disabled esm service", + dir: "testdata/ubuntu_esm_disabled", + releaseID: "ubuntu", + expected: false, + }, + { + name: "esm disabled but +esm package remains installed", + dir: "testdata/ubuntu_esm_disabled", + releaseID: "ubuntu", + packages: []pkg.Package{ + {Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg}, + }, + expected: true, // durable proof: ESM content is on disk even though the channel is now off + }, + { + name: "fips-only pro host is not plain esm", + dir: "testdata/ubuntu_fips_only", + releaseID: "ubuntu", + expected: false, // esm.ubuntu.com/fips must not be folded into the base esm channel + }, + { + name: "non-ubuntu is unaffected by esm evidence", + dir: "testdata/ubuntu_esm_apt", + releaseID: "debian", + expected: false, + }, + { + name: "non-ubuntu is unaffected by esm package version", + dir: "testdata/ubuntu_plain", + releaseID: "debian", + packages: []pkg.Package{ + {Name: "openssl", Version: "1.1.1f-1ubuntu2.19+esm1", Type: pkg.DebPkg}, + }, + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + resolver := fixtureResolverForDir(t, test.dir) + + s := sbom.SBOM{} + s.Artifacts.LinuxDistribution = &linux.Release{ID: test.releaseID} + s.Artifacts.Packages = pkg.NewCollection(test.packages...) + + err := os.DetectFeatures(context.Background(), resolver, sbomsync.NewBuilder(&s)) + require.NoError(t, err) + + require.Equal(t, test.expected, s.Artifacts.LinuxDistribution.ExtendedSupport) + }) + } +} + +// a read error in the apt-source signal must not suppress the ubuntu-advantage status signal; the three +// signals are meant to be independent fallbacks. +func Test_findUbuntuFeatures_signalErrorDoesNotSuppressOthers(t *testing.T) { + resolver := errOnGlobResolver{ + fixtureResolver: fixtureResolverForDir(t, "testdata/ubuntu_esm_status").(fixtureResolver), + errOnGlob: "/etc/apt/sources.list.d/*", + } + + s := sbom.SBOM{} + s.Artifacts.LinuxDistribution = &linux.Release{ID: "ubuntu"} + s.Artifacts.Packages = pkg.NewCollection() + + require.NoError(t, os.DetectFeatures(context.Background(), resolver, sbomsync.NewBuilder(&s))) + + // the apt glob errors, but the status.json signal (esm-infra enabled) should still be consulted + require.True(t, s.Artifacts.LinuxDistribution.ExtendedSupport) +} + +// errOnGlobResolver injects a failure for a specific glob to simulate an unreadable apt directory. +type errOnGlobResolver struct { + fixtureResolver + errOnGlob string +} + +func (r errOnGlobResolver) FilesByGlob(patterns ...string) ([]file.Location, error) { + if slices.Contains(patterns, r.errOnGlob) { + return nil, stdos.ErrPermission + } + return r.fixtureResolver.FilesByGlob(patterns...) +} + +// fixtureResolver maps in-image logical paths to on-disk fixture files so DetectFeatures can be driven +// without building a container image. +type fixtureResolver struct { + file.Resolver // unused methods; nil is fine since only the three below are called + files map[string]string +} + +func (r fixtureResolver) FilesByGlob(patterns ...string) ([]file.Location, error) { + var out []file.Location + for logical := range r.files { + for _, p := range patterns { + if ok, _ := path.Match(p, logical); ok { + out = append(out, file.NewLocation(logical)) + break + } + } + } + return out, nil +} + +func (r fixtureResolver) FilesByPath(paths ...string) ([]file.Location, error) { + var out []file.Location + for _, p := range paths { + if _, ok := r.files[p]; ok { + out = append(out, file.NewLocation(p)) + } + } + return out, nil +} + +func (r fixtureResolver) FileContentsByLocation(l file.Location) (io.ReadCloser, error) { + return stdos.Open(r.files[l.RealPath]) +} + +func fixtureResolverForDir(t *testing.T, dir string) file.Resolver { + t.Helper() + files := map[string]string{} + require.NoError(t, filepath.WalkDir(dir, func(p string, d stdos.DirEntry, err error) error { + if err != nil || d.IsDir() { + return err + } + rel, err := filepath.Rel(dir, p) + if err != nil { + return err + } + files["/"+filepath.ToSlash(rel)] = p + return nil + })) + return fixtureResolver{files: files} +} diff --git a/internal/os/testdata/ubuntu_esm_apt/etc/apt/sources.list.d/ubuntu-esm-infra.list b/internal/os/testdata/ubuntu_esm_apt/etc/apt/sources.list.d/ubuntu-esm-infra.list new file mode 100644 index 000000000..8f43ca9a2 --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_apt/etc/apt/sources.list.d/ubuntu-esm-infra.list @@ -0,0 +1,6 @@ +# Written by ubuntu-advantage-tools +deb https://esm.ubuntu.com/infra/ubuntu focal-infra-security main +# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-security main + +deb https://esm.ubuntu.com/infra/ubuntu focal-infra-updates main +# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-updates main diff --git a/internal/os/testdata/ubuntu_esm_authconf/etc/apt/auth.conf.d/90ubuntu-advantage b/internal/os/testdata/ubuntu_esm_authconf/etc/apt/auth.conf.d/90ubuntu-advantage new file mode 100644 index 000000000..74c7c3018 --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_authconf/etc/apt/auth.conf.d/90ubuntu-advantage @@ -0,0 +1,2 @@ +machine esm.ubuntu.com/infra/ubuntu/ login bearer password DUMMYTOKENREDACTED # ubuntu-pro-client +machine esm.ubuntu.com/apps/ubuntu/ login bearer password DUMMYTOKENREDACTED # ubuntu-pro-client diff --git a/internal/os/testdata/ubuntu_esm_deb822/etc/apt/sources.list.d/ubuntu-esm-infra.sources b/internal/os/testdata/ubuntu_esm_deb822/etc/apt/sources.list.d/ubuntu-esm-infra.sources new file mode 100644 index 000000000..12df00a6e --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_deb822/etc/apt/sources.list.d/ubuntu-esm-infra.sources @@ -0,0 +1,5 @@ +Types: deb +URIs: https://esm.ubuntu.com/infra/ubuntu +Suites: noble-infra-security noble-infra-updates +Components: main +Signed-By: /usr/share/keyrings/ubuntu-pro-esm-infra-keyring.gpg diff --git a/internal/os/testdata/ubuntu_esm_disabled/etc/apt/sources.list.d/ubuntu-esm-infra.list b/internal/os/testdata/ubuntu_esm_disabled/etc/apt/sources.list.d/ubuntu-esm-infra.list new file mode 100644 index 000000000..7351dd71c --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_disabled/etc/apt/sources.list.d/ubuntu-esm-infra.list @@ -0,0 +1,3 @@ +# Written by ubuntu-advantage-tools +# deb https://esm.ubuntu.com/infra/ubuntu focal-infra-security main +# deb-src https://esm.ubuntu.com/infra/ubuntu focal-infra-security main diff --git a/internal/os/testdata/ubuntu_esm_disabled/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_esm_disabled/var/lib/ubuntu-advantage/status.json new file mode 100644 index 000000000..3cd190426 --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_disabled/var/lib/ubuntu-advantage/status.json @@ -0,0 +1,18 @@ +{ + "_schema_version": "0.1", + "attached": true, + "services": [ + { + "name": "esm-apps", + "available": "yes", + "entitled": "yes", + "status": "disabled" + }, + { + "name": "esm-infra", + "available": "yes", + "entitled": "yes", + "status": "disabled" + } + ] +} diff --git a/internal/os/testdata/ubuntu_esm_infra_only/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_esm_infra_only/var/lib/ubuntu-advantage/status.json new file mode 100644 index 000000000..98407f493 --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_infra_only/var/lib/ubuntu-advantage/status.json @@ -0,0 +1,21 @@ +{ + "_schema_version": "0.1", + "attached": true, + "execution_status": "success", + "services": [ + { + "name": "esm-apps", + "description": "Expanded Security Maintenance for Applications", + "available": "yes", + "entitled": "yes", + "status": "disabled" + }, + { + "name": "esm-infra", + "description": "Expanded Security Maintenance for Infrastructure", + "available": "yes", + "entitled": "yes", + "status": "enabled" + } + ] +} diff --git a/internal/os/testdata/ubuntu_esm_malformed/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_esm_malformed/var/lib/ubuntu-advantage/status.json new file mode 100644 index 000000000..dcf314808 --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_malformed/var/lib/ubuntu-advantage/status.json @@ -0,0 +1 @@ +{ this is not valid json diff --git a/internal/os/testdata/ubuntu_esm_status/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_esm_status/var/lib/ubuntu-advantage/status.json new file mode 100644 index 000000000..597f97c09 --- /dev/null +++ b/internal/os/testdata/ubuntu_esm_status/var/lib/ubuntu-advantage/status.json @@ -0,0 +1,28 @@ +{ + "_doc": "Content provided in json response is currently considered Experimental and may change", + "_schema_version": "0.1", + "attached": true, + "services": [ + { + "name": "esm-apps", + "description": "Expanded Security Maintenance for Applications", + "available": "yes", + "entitled": "yes", + "status": "enabled" + }, + { + "name": "esm-infra", + "description": "Expanded Security Maintenance for Infrastructure", + "available": "yes", + "entitled": "yes", + "status": "enabled" + }, + { + "name": "fips", + "description": "NIST-certified FIPS crypto packages", + "available": "no", + "entitled": "no", + "status": "n/a" + } + ] +} diff --git a/internal/os/testdata/ubuntu_fips_only/etc/apt/sources.list.d/ubuntu-fips.list b/internal/os/testdata/ubuntu_fips_only/etc/apt/sources.list.d/ubuntu-fips.list new file mode 100644 index 000000000..14c22bda4 --- /dev/null +++ b/internal/os/testdata/ubuntu_fips_only/etc/apt/sources.list.d/ubuntu-fips.list @@ -0,0 +1,3 @@ +# Written by ubuntu-pro-client +deb https://esm.ubuntu.com/fips/ubuntu focal main +# deb-src https://esm.ubuntu.com/fips/ubuntu focal main diff --git a/internal/os/testdata/ubuntu_fips_only/var/lib/ubuntu-advantage/status.json b/internal/os/testdata/ubuntu_fips_only/var/lib/ubuntu-advantage/status.json new file mode 100644 index 000000000..6926f3151 --- /dev/null +++ b/internal/os/testdata/ubuntu_fips_only/var/lib/ubuntu-advantage/status.json @@ -0,0 +1,28 @@ +{ + "_schema_version": "0.1", + "attached": true, + "execution_status": "success", + "services": [ + { + "name": "esm-apps", + "description": "Expanded Security Maintenance for Applications", + "available": "yes", + "entitled": "yes", + "status": "disabled" + }, + { + "name": "esm-infra", + "description": "Expanded Security Maintenance for Infrastructure", + "available": "yes", + "entitled": "yes", + "status": "disabled" + }, + { + "name": "fips", + "description": "NIST-certified FIPS crypto packages", + "available": "yes", + "entitled": "yes", + "status": "enabled" + } + ] +} diff --git a/internal/os/testdata/ubuntu_plain/etc/apt/sources.list.d/ubuntu.sources b/internal/os/testdata/ubuntu_plain/etc/apt/sources.list.d/ubuntu.sources new file mode 100644 index 000000000..7552b963f --- /dev/null +++ b/internal/os/testdata/ubuntu_plain/etc/apt/sources.list.d/ubuntu.sources @@ -0,0 +1,5 @@ +Types: deb +URIs: http://archive.ubuntu.com/ubuntu/ +Suites: noble noble-updates noble-backports +Components: main restricted universe multiverse +Signed-By: /usr/share/keyrings/ubuntu-archive-keyring.gpg