RuinedYourLife has proposed merging ~ruinedyourlife/launchpad-buildd:clamav-craft-builds into launchpad-buildd:master.
Commit message: Malware scanning for craft builds Requested reviews: Launchpad code reviewers (launchpad-reviewers) For more details, see: https://code.launchpad.net/~ruinedyourlife/launchpad-buildd/+git/launchpad-buildd/+merge/491153 This is a copy paste from the existing implementation for ci builds: `lpbuildd/ci.py` `lpbuildd/tests/test_ci.py` `lpbuildd/target/run_ci.py` `lpbuildd/target/tests/test_run_ci.py` -- Your team Launchpad code reviewers is requested to review the proposed merge of ~ruinedyourlife/launchpad-buildd:clamav-craft-builds into launchpad-buildd:master.
diff --git a/lpbuildd/craft.py b/lpbuildd/craft.py index d61ef34..62d78a3 100644 --- a/lpbuildd/craft.py +++ b/lpbuildd/craft.py @@ -39,6 +39,7 @@ class CraftBuildManager(BuildManagerProxyMixin, DebianBuildManager): self.launchpad_server_url = extra_args.get("launchpad_server_url") self.proxy_service = None self.environment_variables = extra_args.get("environment_variables") + self.scan_malware = extra_args.get("scan_malware", False) super().initiate(files, chroot, extra_args) @@ -73,6 +74,16 @@ class CraftBuildManager(BuildManagerProxyMixin, DebianBuildManager): if self.environment_variables: for key, value in self.environment_variables.items(): args.extend(["--environment-variable", f"{key}={value}"]) + if self.scan_malware: + args.append("--scan-malware") + # Optional ClamAV DB override via builder config (same pattern as CI) + try: + clamav_database_url = self._builder._config.get( + "proxy", "clamavdatabase" + ) + args.extend(["--clamav-database-url", clamav_database_url]) + except Exception: + pass args.append(self.name) self.runTargetSubProcess("build-craft", *args) diff --git a/lpbuildd/target/build_craft.py b/lpbuildd/target/build_craft.py index 81cb27f..6998740 100644 --- a/lpbuildd/target/build_craft.py +++ b/lpbuildd/target/build_craft.py @@ -74,6 +74,16 @@ class BuildCraft( type=str, help="launchpad server url.", ) + parser.add_argument( + "--scan-malware", + action="store_true", + default=False, + help="perform malware scans on output files", + ) + parser.add_argument( + "--clamav-database-url", + help="override default ClamAV database URL", + ) def __init__(self, args, parser): super().__init__(args, parser) @@ -95,6 +105,8 @@ class BuildCraft( if self.backend.is_package_available(dep): deps.append(dep) deps.extend(self.vcs_deps) + if self.args.scan_malware: + deps.append("clamav") # See charmcraft.provider.CharmcraftBuilddBaseConfiguration.setup. self.backend.run(["apt-get", "-y", "install"] + deps) if self.backend.supports_snapd: @@ -141,6 +153,24 @@ class BuildCraft( # We could build the craft in /build, but we are using /home/buildd # for consistency with other build types. self.backend.run(["mkdir", "-p", "/home/buildd"]) + if self.args.scan_malware: + # Ensure ClamAV database is up to date before any scans. + if self.args.clamav_database_url: + with self.backend.open( + "/etc/clamav/freshclam.conf", mode="a" + ) as freshclam_file: + freshclam_file.write( + f"PrivateMirror {self.args.clamav_database_url}\n" + ) + kwargs = {} + env = self.build_proxy_environment( + proxy_url=self.args.proxy_url, + use_fetch_service=self.args.use_fetch_service, + ) + if env: + kwargs["env"] = env + logger.info("Downloading malware definitions...") + self.backend.run(["freshclam", "--quiet"], **kwargs) def repo(self): """Collect git or bzr branch.""" @@ -381,6 +411,14 @@ class BuildCraft( args = ["sourcecraft", "pack", "-v", "--destructive-mode"] self.run_build_command(args, env=env, cwd=build_context_path) + if self.args.scan_malware: + # Scan the output directory for malware after building. + output_path = os.path.join("/home/buildd", self.args.name) + if self.args.build_path is not None: + output_path = os.path.join(output_path, self.args.build_path) + clamscan = ["clamscan", "--recursive", output_path] + self.run_build_command(clamscan, env=env) + def run(self): try: self.install() diff --git a/lpbuildd/target/tests/test_build_craft.py b/lpbuildd/target/tests/test_build_craft.py index 98f2623..839b432 100644 --- a/lpbuildd/target/tests/test_build_craft.py +++ b/lpbuildd/target/tests/test_build_craft.py @@ -1399,3 +1399,195 @@ class TestBuildCraft(TestCase): ] ), ) + + def test_install_scan_malware(self): + args = [ + "build-craft", + "--backend=fake", + "--series=xenial", + "--arch=amd64", + "1", + "--git-repository", + "lp:foo", + "--scan-malware", + "test-image", + ] + build_craft = parse_args(args=args).operation + build_craft.install() + self.assertThat( + build_craft.backend.run.calls, + MatchesListwise( + [ + RanAptGet("install", "git", "clamav"), + RanSnap( + "install", + "--classic", + "--channel=latest/edge", + "sourcecraft", + ), + RanCommand(["mkdir", "-p", "/home/buildd"]), + RanCommand(["freshclam", "--quiet"]), + ] + ), + ) + + def test_install_scan_malware_proxy(self): + args = [ + "build-craft", + "--backend=fake", + "--series=xenial", + "--arch=amd64", + "1", + "--git-repository", + "lp:foo", + "--proxy-url", + "http://proxy.example:3128/", + "--scan-malware", + "test-image", + ] + build_craft = parse_args(args=args).operation + build_craft.bin = "/builderbin" + self.useFixture(FakeFilesystem()).add("/builderbin") + os.mkdir("/builderbin") + with open("/builderbin/lpbuildd-git-proxy", "w") as proxy_script: + proxy_script.write("proxy script\n") + os.fchmod(proxy_script.fileno(), 0o755) + build_craft.install() + env = { + "http_proxy": "http://proxy.example:3128/", + "https_proxy": "http://proxy.example:3128/", + "HTTP_PROXY": "http://proxy.example:3128/", + "HTTPS_PROXY": "http://proxy.example:3128/", + "GIT_PROXY_COMMAND": "/usr/local/bin/lpbuildd-git-proxy", + "SNAPPY_STORE_NO_CDN": "1", + } + self.assertThat( + build_craft.backend.run.calls, + MatchesListwise( + [ + RanAptGet( + "install", + "python3", + "socat", + "git", + "clamav", + ), + RanSnap( + "install", + "--classic", + "--channel=latest/edge", + "sourcecraft", + ), + RanCommand(["mkdir", "-p", "/home/buildd"]), + RanCommand(["freshclam", "--quiet"], **env), + ] + ), + ) + self.assertEqual( + (b"proxy script\n", stat.S_IFREG | 0o755), + build_craft.backend.backend_fs[ + "/usr/local/bin/lpbuildd-git-proxy" + ], + ) + + def test_install_scan_malware_with_clamav_database_url(self): + args = [ + "build-craft", + "--backend=fake", + "--series=xenial", + "--arch=amd64", + "1", + "--git-repository", + "lp:foo", + "--scan-malware", + "--clamav-database-url", + "http://clamav.example/", + "test-image", + ] + build_craft = parse_args(args=args).operation + build_craft.backend.add_file( + "/etc/clamav/freshclam.conf", b"Test line\n" + ) + build_craft.install() + self.assertThat( + build_craft.backend.run.calls, + MatchesListwise( + [ + RanAptGet("install", "git", "clamav"), + RanSnap( + "install", + "--classic", + "--channel=latest/edge", + "sourcecraft", + ), + RanCommand(["mkdir", "-p", "/home/buildd"]), + RanCommand(["freshclam", "--quiet"]), + ] + ), + ) + self.assertEqual( + ( + b"Test line\nPrivateMirror http://clamav.example/\n", + stat.S_IFREG | 0o644, + ), + build_craft.backend.backend_fs["/etc/clamav/freshclam.conf"], + ) + + def test_build_scan_malware_succeeds(self): + args = [ + "build-craft", + "--backend=fake", + "--series=xenial", + "--arch=amd64", + "1", + "--branch", + "lp:foo", + "--scan-malware", + "test-image", + ] + build_craft = parse_args(args=args).operation + build_craft.backend.add_dir("/build/test-directory") + build_craft.build() + self.assertThat( + build_craft.backend.run.calls, + MatchesListwise( + [ + RanBuildCommand( + ["sourcecraft", "pack", "-v", "--destructive-mode"], + cwd="/home/buildd/test-image/.", + ), + RanBuildCommand( + [ + "clamscan", + "--recursive", + "/home/buildd/test-image/.", + ], + cwd="/home/buildd/test-image", + ), + ] + ), + ) + + def test_run_scan_malware_fails(self): + class FailClamscan(FakeMethod): + def __call__(self, run_args, *args, **kwargs): + super().__call__(run_args, *args, **kwargs) + if run_args[0] == "clamscan": + raise subprocess.CalledProcessError(1, run_args) + + self.useFixture(FakeLogger()) + args = [ + "build-craft", + "--backend=fake", + "--series=xenial", + "--arch=amd64", + "1", + "--branch", + "lp:foo", + "--scan-malware", + "test-image", + ] + build_craft = parse_args(args=args).operation + build_craft.backend.build_path = self.useFixture(TempDir()).path + build_craft.backend.run = FailClamscan() + self.assertEqual(RETCODE_FAILURE_BUILD, build_craft.run()) diff --git a/lpbuildd/tests/test_craft.py b/lpbuildd/tests/test_craft.py index b9dfa40..fd38c3e 100644 --- a/lpbuildd/tests/test_craft.py +++ b/lpbuildd/tests/test_craft.py @@ -269,3 +269,41 @@ class TestCraftBuildManagerIteration(TestCase): "launchpad.test", ] yield self.startBuild(args, expected_options) + + @defer.inlineCallbacks + def test_iterate_with_scan_malware(self): + # The build manager passes --scan-malware to subprocesses. + args = { + "git_repository": "https://git.launchpad.dev/~example/+git/craft", + "git_path": "master", + "scan_malware": True, + } + expected_options = [ + "--git-repository", + "https://git.launchpad.dev/~example/+git/craft", + "--git-path", + "master", + "--scan-malware", + ] + yield self.startBuild(args, expected_options) + + @defer.inlineCallbacks + def test_iterate_with_clamav_database_url(self): + # If proxy.clamavdatabase is set, the manager passes it via + # the --clamav-database-url option. + self.buildmanager._builder._config.set( + "proxy", "clamavdatabase", "http://clamav.example/" + ) + args = { + "git_repository": "https://git.launchpad.dev/~example/+git/craft", + "git_path": "master", + } + expected_options = [ + "--git-repository", + "https://git.launchpad.dev/~example/+git/craft", + "--git-path", + "master", + "--clamav-database-url", + "http://clamav.example/", + ] + yield self.startBuild(args, expected_options)
_______________________________________________ Mailing list: https://launchpad.net/~launchpad-reviewers Post to : launchpad-reviewers@lists.launchpad.net Unsubscribe : https://launchpad.net/~launchpad-reviewers More help : https://help.launchpad.net/ListHelp