~ubuntu-core-dev/update-manager/main

2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
1
#!/usr/bin/python3
2
3
import apt
4
import argparse
5
import distro_info
2926 by Brian Murray
ubuntu-security-status: use ubuntu-advantage-tools to determine whether or
6
import json
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
7
import os
8
import sys
9
import gettext
2949.1.2 by Renan Rodrigo
Prefer redirecting ubuntu-security-status to pro security-status if it exists
10
import shutil
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
11
import subprocess
12
13
from UpdateManager.Core.utils import get_dist
14
15
from datetime import datetime
16
from textwrap import wrap
17
from urllib.error import URLError, HTTPError
18
from urllib.request import urlopen
19
20
# TODO make DEBUG an environmental variable
21
DEBUG = False
2926 by Brian Murray
ubuntu-security-status: use ubuntu-advantage-tools to determine whether or
22
UA_STATUS_FILE = "/var/lib/ubuntu-advantage/status.json"
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
23
24
25
class PatchStats:
26
    """Tracks overall patch status
27
28
    The relationship between archives enabled and whether a patch is eligible
29
    for receiving updates is non-trivial. We track here all the important
30
    buckets a package can be in:
31
32
        - Whether it is set to expire with no ESM coverage
33
        - Whether it is in an archive covered by ESM
34
        - Whether it received LTS patches
35
        - whether it received ESM patches
36
37
    We also track the total packages covered and uncovered, and for the
38
    uncovered packages, we track where they originate from.
39
40
    The Ubuntu main archive receives patches for 5 years.
41
    Canonical-owned archives (excluding partner) receive patches for 10 years.
42
        patches for 10 years.
43
    """
44
    def __init__(self):
45
        # TODO no-update FIPS is never patched
46
        self.pkgs_uncovered_fips = set()
47
48
        # list of package names available in ESM
49
        self.pkgs_updated_in_esmi = set()
50
        self.pkgs_updated_in_esma = set()
51
52
        self.pkgs_mr = set()
53
        self.pkgs_um = set()
54
        self.pkgs_unavailable = set()
55
        self.pkgs_thirdparty = set()
56
        # the bin of unknowns
57
        self.pkgs_uncategorized = set()
58
59
60
def print_debug(s):
61
    if DEBUG:
62
        print(s)
63
64
65
def whats_in_esm(url):
66
    pkgs = set()
67
    # return a set of package names in an esm archive
68
    try:
69
        response = urlopen(url)
70
    except (URLError, HTTPError):
71
        print_debug('failed to load: %s' % url)
72
        return pkgs
73
    try:
74
        content = response.read().decode('utf-8')
75
    except IOError:
76
        print('failed to read data at: %s' % url)
77
        sys.exit(1)
78
    for line in content.split('\n'):
79
        if not line.startswith('Package:'):
80
            continue
81
        else:
82
            pkg = line.split(': ')[1]
83
            pkgs.add(pkg)
84
    return pkgs
85
86
2926 by Brian Murray
ubuntu-security-status: use ubuntu-advantage-tools to determine whether or
87
def get_ua_status():
88
    """Return dict of active ua status information from ubuntu-advantage-tools
89
90
    Prefer to obtain status information from cache on disk to avoid costly
91
    roundtrips to contracts.canonical.com to check on available services.
92
93
    Fallback to call ua status --format=json.
94
95
    If there are errors running: ua status --format=json or if the status on
96
    disk is unparseable, return an empty dict.
97
    """
98
    if os.path.exists(UA_STATUS_FILE):
99
        with open(UA_STATUS_FILE, "r") as stream:
100
            status = stream.read()
101
    else:
102
        try:
103
            status = subprocess.check_output(
104
                ['ua', 'status', '--format=json']
105
            ).decode()
106
        except subprocess.CalledProcessError as e:
107
            print_debug('failed to run ua status: %s' % e)
108
            return {}
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
109
    try:
2926 by Brian Murray
ubuntu-security-status: use ubuntu-advantage-tools to determine whether or
110
        return json.loads(status)
111
    except json.decoder.JSONDecodeError as e:
112
        print_debug('failed to parse JSON from ua status output: %s' % e)
113
        return {}
114
115
116
def is_ua_service_enabled(service_name: str) -> bool:
117
    """Check to see if named esm service is enabled.
118
119
    :return: True if UA status reports service as enabled.
120
    """
121
    status = get_ua_status()
122
    for service in status.get("services", []):
123
        if service["name"] == service_name:
124
            # Machines unattached to UA will not provide service 'status' key.
125
            return service.get("status") == "enabled"
126
    return False
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
127
128
129
def trim_archive(archive):
130
    return archive.split("-")[-1]
131
132
133
def trim_site(host):
134
    # *.ec2.archive.ubuntu.com -> archive.ubuntu.com
135
    if host.endswith("archive.ubuntu.com"):
136
        return "archive.ubuntu.com"
137
    return host
138
139
140
def mirror_list():
141
    m_file = '/usr/share/ubuntu-release-upgrader/mirrors.cfg'
142
    if not os.path.exists(m_file):
143
        print("Official mirror list not found.")
144
    with open(m_file) as f:
145
        items = [x.strip() for x in f]
146
    mirrors = [s.split('//')[1].split('/')[0] for s in items
147
               if not s.startswith("#") and not s == ""]
148
    # ddebs.ubuntu.com isn't in mirrors.cfg for every release
149
    mirrors.append('ddebs.ubuntu.com')
150
    return mirrors
151
152
153
def origins_for(ver: apt.package.Version) -> str:
154
    s = []
155
    for origin in ver.origins:
156
        if not origin.site:
157
            # When the package is installed, site is empty, archive/component
158
            # are "now/now"
159
            continue
160
        site = trim_site(origin.site)
161
        s.append("%s %s/%s" % (site, origin.archive, origin.component))
162
    return ",".join(s)
163
164
165
def print_wrapped(str):
166
    print("\n".join(wrap(str, break_on_hyphens=False)))
167
168
169
def print_thirdparty_count():
170
    print(gettext.dngettext("update-manager",
171
                            "%s package is from a third party",
172
                            "%s packages are from third parties",
173
                            len(pkgstats.pkgs_thirdparty)) %
174
          "{:>{width}}".format(len(pkgstats.pkgs_thirdparty), width=width))
175
176
177
def print_unavailable_count():
178
    print(gettext.dngettext("update-manager",
179
                            "%s package is no longer available for "
180
                            "download",
181
                            "%s packages are no longer available for "
182
                            "download",
183
                            len(pkgstats.pkgs_unavailable)) %
184
          "{:>{width}}".format(len(pkgstats.pkgs_unavailable), width=width))
185
186
187
def parse_options():
188
    '''Parse command line arguments.
189
190
    Return parser
191
    '''
192
    parser = argparse.ArgumentParser(
193
        description='Return information about security support for packages')
194
    parser.add_argument('--thirdparty', action='store_true')
195
    parser.add_argument('--unavailable', action='store_true')
196
    return parser
197
198
199
if __name__ == "__main__":
2949.1.2 by Renan Rodrigo
Prefer redirecting ubuntu-security-status to pro security-status if it exists
200
    # Prefer redirecting to 'pro security-status' if it exists
201
    if shutil.which("/usr/bin/pro"):
202
        print("This command has been replaced with 'pro security-status'.")
203
        subprocess.run(["/usr/bin/pro", "security-status"])
204
        sys.exit(0)
205
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
206
    # gettext
207
    APP = "update-manager"
208
    DIR = "/usr/share/locale"
209
    gettext.bindtextdomain(APP, DIR)
210
    gettext.textdomain(APP)
211
212
    parser = parse_options()
213
    args = parser.parse_args()
214
215
    esm_site = "esm.ubuntu.com"
216
217
    try:
218
        dpkg = subprocess.check_output(['dpkg', '--print-architecture'])
219
        arch = dpkg.decode().strip()
220
    except subprocess.CalledProcessError:
221
        print("failed getting dpkg architecture")
222
        sys.exit(1)
223
224
    cache = apt.Cache()
225
    pkgstats = PatchStats()
226
    codename = get_dist()
227
    di = distro_info.UbuntuDistroInfo()
228
    lts = di.is_lts(codename)
229
    release_expired = True
230
    if codename in di.supported():
231
        release_expired = False
232
    # distro-info-data in Ubuntu 16.04 LTS does not have eol-esm data
233
    if codename != 'xenial':
234
        eol_data = [(r.eol, r.eol_esm)
235
                    for r in di._releases if r.series == codename][0]
236
    elif codename == 'xenial':
237
        eol_data = (datetime.strptime('2021-04-21', '%Y-%m-%d'),
238
                    datetime.strptime('2024-04-21', '%Y-%m-%d'))
239
    eol = eol_data[0]
240
    eol_esm = eol_data[1]
241
242
    all_origins = set()
243
    origins_by_package = {}
244
    official_mirrors = mirror_list()
245
246
    # N.B. only the security pocket is checked because this tool displays
247
    # information about security updates
248
    esm_url = \
249
        'https://%s/%s/ubuntu/dists/%s-%s-%s/main/binary-%s/Packages'
250
    pkgs_in_esma = whats_in_esm(esm_url %
251
                                (esm_site, 'apps', codename, 'apps',
252
                                 'security', arch))
253
    pkgs_in_esmi = whats_in_esm(esm_url %
254
                                (esm_site, 'infra', codename, 'infra',
255
                                 'security', arch))
256
257
    for pkg in cache:
258
        pkgname = pkg.name
259
260
        downloadable = True
261
        if not pkg.is_installed:
262
            continue
263
        if not pkg.candidate or not pkg.candidate.downloadable:
264
            downloadable = False
265
        pkg_sites = []
266
        origins_by_package[pkgname] = set()
267
268
        for ver in pkg.versions:
269
            # Loop through origins and store all of them. The idea here is that
270
            # we don't care where the installed package comes from, provided
271
            # there is at least one repository we identify as being
272
            # security-assured under either LTS or ESM.
273
            for origin in ver.origins:
274
                # TODO: in order to handle FIPS and other archives which have
275
                # root-level path names, we'll need to loop over ver.uris
276
                # instead
277
                if not origin.site:
278
                    continue
279
                site = trim_site(origin.site)
280
                archive = origin.archive
281
                component = origin.component
282
                origin = origin.origin
283
                official_mirror = False
284
                thirdparty = True
285
                # thirdparty providers like dl.google.com don't set "Origin"
286
                if origin != "Ubuntu":
287
                    thirdparty = False
288
                if site in official_mirrors:
289
                    site = "official_mirror"
290
                if "MY_MIRROR" in os.environ:
291
                    if site in os.environ["MY_MIRROR"]:
292
                        site = "official_mirror"
293
                t = (site, archive, component, thirdparty)
294
                if not site:
295
                    continue
296
                all_origins.add(t)
297
                origins_by_package[pkgname].add(t)
298
299
            if DEBUG:
300
                pkg_sites.append("%s %s/%s" %
301
                                 (site, archive, component))
302
303
        print_debug("available versions for %s" % pkgname)
304
        print_debug(",".join(pkg_sites))
305
306
    # This tracks suites we care about. Sadly, it appears that the way apt
307
    # stores origins truncates away the path that comes after the
308
    # domainname in the site portion, or maybe I am just clueless, but
309
    # there's no way to tell FIPS apart from ESM, for instance.
310
    # See 00REPOS.txt for examples
311
312
    # 2020-03-18 ver.filename has the path so why is that no good?
313
314
    # TODO Need to handle:
315
    #   MAAS, lxd, juju PPAs
316
    #   other PPAs
317
    #   other repos
318
319
    # TODO handle partner.c.c
320
321
    # main and restricted from release, -updates, -proposed, or -security
322
    # pockets
323
    suite_main = ("official_mirror", codename, "main", True)
324
    suite_main_updates = ("official_mirror", codename + "-updates",
325
                          "main", True)
326
    suite_main_security = ("official_mirror", codename + "-security",
327
                           "main", True)
328
    suite_main_proposed = ("official_mirror", codename + "-proposed",
329
                           "main", True)
330
331
    suite_restricted = ("official_mirror", codename, "restricted",
332
                        True)
333
    suite_restricted_updates = ("official_mirror",
334
                                codename + "-updates",
335
                                "restricted", True)
336
    suite_restricted_security = ("official_mirror",
337
                                 codename + "-security",
338
                                 "restricted", True)
339
    suite_restricted_proposed = ("official_mirror",
340
                                 codename + "-proposed",
341
                                 "restricted", True)
342
343
    # universe and multiverse from release, -updates, -proposed, or -security
344
    # pockets
345
    suite_universe = ("official_mirror", codename, "universe", True)
346
    suite_universe_updates = ("official_mirror", codename + "-updates",
347
                              "universe", True)
348
    suite_universe_security = ("official_mirror",
349
                               codename + "-security",
350
                               "universe", True)
351
    suite_universe_proposed = ("official_mirror",
352
                               codename + "-proposed",
353
                               "universe", True)
354
355
    suite_multiverse = ("official_mirror", codename, "multiverse",
356
                        True)
357
    suite_multiverse_updates = ("official_mirror",
358
                                codename + "-updates",
359
                                "multiverse", True)
360
    suite_multiverse_security = ("official_mirror",
361
                                 codename + "-security",
362
                                 "multiverse", True)
363
    suite_multiverse_proposed = ("official_mirror",
364
                                 codename + "-proposed",
365
                                 "multiverse", True)
366
367
    # packages from the esm respositories
368
    # N.B. Origin: Ubuntu is not set for esm
369
    suite_esm_main = (esm_site, "%s-infra-updates" % codename,
370
                      "main")
371
    suite_esm_main_security = (esm_site,
372
                               "%s-infra-security" % codename, "main")
373
    suite_esm_universe = (esm_site,
374
                          "%s-apps-updates" % codename, "main")
375
    suite_esm_universe_security = (esm_site,
376
                                   "%s-apps-security" % codename,
377
                                   "main")
378
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
379
    esm_infra_enabled = is_ua_service_enabled("esm-infra")
380
    esm_apps_enabled = is_ua_service_enabled("esm-apps")
2926 by Brian Murray
ubuntu-security-status: use ubuntu-advantage-tools to determine whether or
381
    ua_attached = get_ua_status().get("attached", False)
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
382
383
    # Now do the final loop through
384
    for pkg in cache:
385
        if not pkg.is_installed:
386
            continue
387
        if not pkg.candidate or not pkg.candidate.downloadable:
388
            pkgstats.pkgs_unavailable.add(pkg.name)
389
            continue
390
        pkgname = pkg.name
391
        pkg_origins = origins_by_package[pkgname]
392
393
        # This set of is_* booleans tracks specific situations we care about in
394
        # the logic below; for instance, if the package has a main origin, or
395
        # if the esm repos are enabled.
396
397
        # Some packages get added in -updates and don't exist in the release
398
        # pocket e.g. ubuntu-advantage-tools and libdrm-updates. To be safe all
399
        # pockets are allowed.
400
        is_mr_pkg_origin = (suite_main in pkg_origins) or \
401
                           (suite_main_updates in pkg_origins) or \
402
                           (suite_main_security in pkg_origins) or \
403
                           (suite_main_proposed in pkg_origins) or \
404
                           (suite_restricted in pkg_origins) or \
405
                           (suite_restricted_updates in pkg_origins) or \
406
                           (suite_restricted_security in pkg_origins) or \
407
                           (suite_restricted_proposed in pkg_origins)
408
        is_um_pkg_origin = (suite_universe in pkg_origins) or \
409
                           (suite_universe_updates in pkg_origins) or \
410
                           (suite_universe_security in pkg_origins) or \
411
                           (suite_universe_proposed in pkg_origins) or \
412
                           (suite_multiverse in pkg_origins) or \
413
                           (suite_multiverse_updates in pkg_origins) or \
414
                           (suite_multiverse_security in pkg_origins) or \
415
                           (suite_multiverse_proposed in pkg_origins)
416
417
        is_esm_infra_pkg_origin = (suite_esm_main in pkg_origins) or \
418
                                  (suite_esm_main_security in pkg_origins)
419
        is_esm_apps_pkg_origin = (suite_esm_universe in pkg_origins) or \
420
                                 (suite_esm_universe_security in pkg_origins)
421
422
        # A third party one won't appear in any of the above origins
423
        if not is_mr_pkg_origin and not is_um_pkg_origin \
424
                and not is_esm_infra_pkg_origin and not is_esm_apps_pkg_origin:
425
            pkgstats.pkgs_thirdparty.add(pkgname)
426
427
        if False:  # TODO package has ESM fips origin
428
            # TODO package has ESM fips-updates origin: OK
429
            # If user has enabled FIPS, but not updates, BAD, but need some
430
            # thought on how to display it, as it can't be patched at all
431
            pass
432
        elif is_mr_pkg_origin:
433
            pkgstats.pkgs_mr.add(pkgname)
434
        elif is_um_pkg_origin:
435
            pkgstats.pkgs_um.add(pkgname)
436
        else:
437
            # TODO print information about packages in this category if in
438
            # debugging mode
439
            pkgstats.pkgs_uncategorized.add(pkgname)
440
441
        # Check to see if the package is available in esm-infra or esm-apps
442
        # and add it to the right pkgstats category
443
        # NB: apps is ordered first for testing the hello package which is both
444
        # in esmi and esma
445
        if pkgname in pkgs_in_esma:
446
            pkgstats.pkgs_updated_in_esma.add(pkgname)
447
        elif pkgname in pkgs_in_esmi:
448
            pkgstats.pkgs_updated_in_esmi.add(pkgname)
449
450
    total_packages = (len(pkgstats.pkgs_mr) +
451
                      len(pkgstats.pkgs_um) +
452
                      len(pkgstats.pkgs_thirdparty) +
453
                      len(pkgstats.pkgs_unavailable))
454
    width = len(str(total_packages))
455
    print("%s packages installed, of which:" %
456
          "{:>{width}}".format(total_packages, width=width))
457
458
    # filters first as they provide less information
459
    if args.thirdparty:
460
        if pkgstats.pkgs_thirdparty:
461
            pkgs_thirdparty = sorted(p for p in pkgstats.pkgs_thirdparty)
462
            print_thirdparty_count()
463
            print_wrapped(' '.join(pkgs_thirdparty))
464
            msg = ("Packages from third parties are not provided by the "
465
                   "official Ubuntu archive, for example packages from "
466
                   "Personal Package Archives in Launchpad.")
467
            print("")
468
            print_wrapped(msg)
469
            print("")
470
            print_wrapped("Run 'apt-cache policy %s' to learn more about "
471
                          "that package." % pkgs_thirdparty[0])
472
            sys.exit(0)
473
        else:
474
            print_wrapped("You have no packages installed from a third party.")
475
            sys.exit(0)
476
    if args.unavailable:
477
        if pkgstats.pkgs_unavailable:
478
            pkgs_unavailable = sorted(p for p in pkgstats.pkgs_unavailable)
479
            print_unavailable_count()
480
            print_wrapped(' '.join(pkgs_unavailable))
481
            msg = ("Packages that are not available for download "
482
                   "may be left over from a previous release of "
483
                   "Ubuntu, may have been installed directly from "
484
                   "a .deb file, or are from a source which has "
485
                   "been disabled.")
486
            print("")
487
            print_wrapped(msg)
488
            print("")
489
            print_wrapped("Run 'apt-cache show %s' to learn more about "
490
                          "that package." % pkgs_unavailable[0])
491
            sys.exit(0)
492
        else:
493
            print_wrapped("You have no packages installed that are no longer "
494
                          "available.")
495
            sys.exit(0)
496
    # Only show LTS patches and expiration notices if the release is not
497
    # yet expired; showing LTS patches would give a false sense of
498
    # security.
499
    if not release_expired:
500
        print("%s receive package updates%s until %d/%d" %
501
              ("{:>{width}}".format(len(pkgstats.pkgs_mr),
502
                                    width=width),
503
               " with LTS" if lts else "",
504
               eol.month, eol.year))
505
    elif release_expired and lts:
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
506
        receive_text = "could receive"
507
        if esm_infra_enabled:
508
            if len(pkgstats.pkgs_mr) == 1:
509
                receive_text = "is receiving"
510
            else:
511
                receive_text = "are receiving"
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
512
        print("%s %s security updates with ESM Infra "
513
              "until %d/%d" %
514
              ("{:>{width}}".format(len(pkgstats.pkgs_mr),
515
                                    width=width),
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
516
               receive_text, eol_esm.month, eol_esm.year))
2949.1.1 by Renan Rodrigo
Do not show ESM Apps information in ubuntu-security-status if the service is not enabled
517
    if lts and pkgstats.pkgs_um and is_ua_service_enabled("esm-apps"):
518
        if len(pkgstats.pkgs_um) == 1:
519
            receive_text = "is receiving"
520
        else:
521
            receive_text = "are receiving"
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
522
        print("%s %s security updates with ESM Apps "
523
              "until %d/%d" %
524
              ("{:>{width}}".format(len(pkgstats.pkgs_um),
525
                                    width=width),
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
526
               receive_text, eol_esm.month, eol_esm.year))
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
527
    if pkgstats.pkgs_thirdparty:
528
        print_thirdparty_count()
529
    if pkgstats.pkgs_unavailable:
530
        print_unavailable_count()
531
    # print the detail messages after the count of packages
532
    if pkgstats.pkgs_thirdparty:
533
        msg = ("Packages from third parties are not provided by the "
534
               "official Ubuntu archive, for example packages from "
535
               "Personal Package Archives in Launchpad.")
536
        print("")
537
        print_wrapped(msg)
538
        action = ("For more information on the packages, run "
539
                  "'ubuntu-security-status --thirdparty'.")
540
        print_wrapped(action)
541
    if pkgstats.pkgs_unavailable:
542
        msg = ("Packages that are not available for download "
543
               "may be left over from a previous release of "
544
               "Ubuntu, may have been installed directly from "
545
               "a .deb file, or are from a source which has "
546
               "been disabled.")
547
        print("")
548
        print_wrapped(msg)
549
        action = ("For more information on the packages, run "
550
                  "'ubuntu-security-status --unavailable'.")
551
        print_wrapped(action)
552
    # print the ESM calls to action last
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
553
    if lts and not esm_infra_enabled:
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
554
        if release_expired and pkgstats.pkgs_mr:
555
            pkgs_updated_in_esmi = pkgstats.pkgs_updated_in_esmi
556
            print("")
2939 by Brian Murray
ubuntu-security-status: further improvements to pluralization and phrasing
557
            msg = gettext.dngettext(
558
                "update-manager",
559
                "Enable Extended Security "
560
                "Maintenance (ESM Infra) to "
561
                "get %i security update (so far) ",
562
                "Enable Extended Security "
563
                "Maintenance (ESM Infra) to "
564
                "get %i security updates (so far) ",
565
                len(pkgs_updated_in_esmi)) % len(pkgs_updated_in_esmi)
566
            msg += gettext.dngettext(
567
                "update-manager",
568
                "and enable coverage of %i "
569
                "package.",
570
                "and enable coverage of %i "
571
                "packages.",
572
                len(pkgstats.pkgs_mr)) % len(pkgstats.pkgs_mr)
573
            print_wrapped(msg)
2937 by Brian Murray
ubuntu-security-status: Check if ESM for Apps is enabled or if it is not
574
            if ua_attached:
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
575
                print("\nEnable ESM Infra with: ua enable esm-infra")
2949.1.1 by Renan Rodrigo
Do not show ESM Apps information in ubuntu-security-status if the service is not enabled
576
2926 by Brian Murray
ubuntu-security-status: use ubuntu-advantage-tools to determine whether or
577
    if lts and not ua_attached:
2884 by Brian Murray
Add ubuntu-security-status - a tool for displaying information about
578
        print("\nThis machine is not attached to an Ubuntu Advantage "
579
              "subscription.\nSee https://ubuntu.com/advantage")