devshield-scanner 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devshield-scanner
3
- Version: 0.1.0
4
- Summary: Read-only Linux host and Docker filesystem inspection using ClamAV
3
+ Version: 0.2.0
4
+ Summary: Linux host/Docker inspection with opt-in response for detected process executables
5
5
  License-Expression: MIT
6
6
  Classifier: Development Status :: 3 - Alpha
7
7
  Classifier: Environment :: Console
@@ -14,7 +14,17 @@ Dynamic: license-file
14
14
 
15
15
  # DevShield for Linux
16
16
 
17
- An installable Python CLI package is included. See **INSTALL.md** to build a wheel and install the `devshield` command on servers using an isolated environment under `/opt/devshield`. There is no published npm or Python registry package yet. `devshield --version` displays the CLI version; existing scanner flags are unchanged.
17
+ ## Process-first response (0.2.0)
18
+
19
+ `sudo /opt/devshield/bin/devshield --remediate-running` first snapshots and scans running executables, sends SIGKILL to signature-detected processes, and attempts to move matching host executables into `/var/lib/devshield/quarantine`. It then scans host files and Docker data. Without this flag, scanning remains read-only. This is an experimental opt-in response mode and can interrupt production services. Process response currently requires root and Python/Linux pidfd support (normally Python 3.9+ and Linux 5.3+).
20
+
21
+ Heuristic, PUA, encrypted-content, scan-limit and EICAR test alerts never trigger automatic process response. Evidence copies and action records are saved before termination. PID 1, DevShield and its ancestors are excluded. Original-file moves require a matching inode and a same-filesystem rename; failures are reported rather than falling back to destructive copy/delete. Container executables are not moved: rebuild affected containers from trusted images. Supervisors may restart terminated processes. Script interpreters, injected code, memory-only malware and persistence are not comprehensively inspected, and files found during the subsequent host/container scan are not automatically removed. Quarantine samples must not be executed. Quarantine is excluded from later scans.
22
+
23
+ `--max-files 200000` raises the archive member-count limit when needed. Example for the large container export discussed during validation: `devshield --remediate-running --max-files 200000 --max-filesize-mb 1536 --max-scansize-mb 2048`. Limits do not apply to process snapshots, which retain ClamAV defaults with limit alerts. Increasing limits raises resource use. A privileged attacker can still evade or interfere with live-host inspection. There is no guarantee of complete malware removal.
24
+
25
+ An installable Python CLI package is included. The Python distribution is `devshield-scanner`; the command is `devshield`. See **INSTALL.md** for isolated installation under `/opt/devshield`. `devshield --version` displays the CLI version.
26
+
27
+ Version 0.1.1 enables ClamAV scan-limit alerts and marks zero-data scans, missing summaries and logged warnings/errors as incomplete coverage (exit 2), even when ClamAV returns 0. Limit alerts are not proof of malware. Optional `--max-filesize-mb` and `--max-scansize-mb` override engine limits in MB. Increasing limits can increase memory, CPU and runtime; engine hard limits still apply. A positive data count alone does not prove every archive entry was inspected.
18
28
 
19
29
  Read-only host and Docker filesystem malware scanning using the locally installed ClamAV signature engine. Requires Linux, Python 3.8+, `clamscan`, updated ClamAV databases, and the Docker CLI for container scanning. Run on the Docker host with a local Unix socket context. Root access normally provides the required file/socket access.
20
30
 
@@ -1,6 +1,16 @@
1
1
  # DevShield for Linux
2
2
 
3
- An installable Python CLI package is included. See **INSTALL.md** to build a wheel and install the `devshield` command on servers using an isolated environment under `/opt/devshield`. There is no published npm or Python registry package yet. `devshield --version` displays the CLI version; existing scanner flags are unchanged.
3
+ ## Process-first response (0.2.0)
4
+
5
+ `sudo /opt/devshield/bin/devshield --remediate-running` first snapshots and scans running executables, sends SIGKILL to signature-detected processes, and attempts to move matching host executables into `/var/lib/devshield/quarantine`. It then scans host files and Docker data. Without this flag, scanning remains read-only. This is an experimental opt-in response mode and can interrupt production services. Process response currently requires root and Python/Linux pidfd support (normally Python 3.9+ and Linux 5.3+).
6
+
7
+ Heuristic, PUA, encrypted-content, scan-limit and EICAR test alerts never trigger automatic process response. Evidence copies and action records are saved before termination. PID 1, DevShield and its ancestors are excluded. Original-file moves require a matching inode and a same-filesystem rename; failures are reported rather than falling back to destructive copy/delete. Container executables are not moved: rebuild affected containers from trusted images. Supervisors may restart terminated processes. Script interpreters, injected code, memory-only malware and persistence are not comprehensively inspected, and files found during the subsequent host/container scan are not automatically removed. Quarantine samples must not be executed. Quarantine is excluded from later scans.
8
+
9
+ `--max-files 200000` raises the archive member-count limit when needed. Example for the large container export discussed during validation: `devshield --remediate-running --max-files 200000 --max-filesize-mb 1536 --max-scansize-mb 2048`. Limits do not apply to process snapshots, which retain ClamAV defaults with limit alerts. Increasing limits raises resource use. A privileged attacker can still evade or interfere with live-host inspection. There is no guarantee of complete malware removal.
10
+
11
+ An installable Python CLI package is included. The Python distribution is `devshield-scanner`; the command is `devshield`. See **INSTALL.md** for isolated installation under `/opt/devshield`. `devshield --version` displays the CLI version.
12
+
13
+ Version 0.1.1 enables ClamAV scan-limit alerts and marks zero-data scans, missing summaries and logged warnings/errors as incomplete coverage (exit 2), even when ClamAV returns 0. Limit alerts are not proof of malware. Optional `--max-filesize-mb` and `--max-scansize-mb` override engine limits in MB. Increasing limits can increase memory, CPU and runtime; engine hard limits still apply. A positive data count alone does not prove every archive entry was inspected.
4
14
 
5
15
  Read-only host and Docker filesystem malware scanning using the locally installed ClamAV signature engine. Requires Linux, Python 3.8+, `clamscan`, updated ClamAV databases, and the Docker CLI for container scanning. Run on the Docker host with a local Unix socket context. Root access normally provides the required file/socket access.
6
16
 
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env python3
2
- """Read-only Linux host/Docker filesystem scan using the installed ClamAV engine."""
2
+ """Linux host/Docker inspection with opt-in response for detected running executables."""
3
3
  import argparse
4
4
  import contextlib
5
5
  import datetime as dt
@@ -12,11 +12,36 @@ import subprocess
12
12
  import sys
13
13
  import tempfile
14
14
  import time
15
+ from devshield_processes import inspect_running
16
+
17
+
18
+ def coverage_warnings(log_path):
19
+ """Read logs incrementally; a zero exit code alone does not establish coverage."""
20
+ warnings = set()
21
+ summary = False
22
+ with open(log_path, encoding='utf-8', errors='replace') as log:
23
+ for line in log:
24
+ if 'Data scanned:' in line:
25
+ summary = True
26
+ if re.search(r'Data scanned:\s*0(?:[.,]0+)?\s', line):
27
+ warnings.add('ClamAV reported zero data scanned; coverage is unverified.')
28
+ if 'Heuristics.Limits.Exceeded' in line or 'Heuristics.Encrypted.' in line:
29
+ warnings.add('Scan limit or encrypted-content alert; some content was not inspected.')
30
+ if re.search(r'\b(?:WARNING|ERROR):', line) or re.search(r'Total errors:\s*[1-9]', line):
31
+ warnings.add('ClamAV reported warnings/errors; inspect the stage log.')
32
+ if not summary:
33
+ warnings.add('ClamAV scan summary missing; coverage is unverified.')
34
+ return sorted(warnings)
15
35
 
16
36
 
17
37
  def main():
18
38
  parser = argparse.ArgumentParser(description=__doc__)
19
- parser.add_argument('--version', action='version', version='DevShield 0.1.0')
39
+ parser.add_argument('--version', action='version', version='DevShield 0.2.0')
40
+ parser.add_argument('--remediate-running', action='store_true', help='FIRST scan process executables; kill signature-detected processes and quarantine host binaries. Can interrupt services.')
41
+ parser.add_argument('--quarantine', default='/var/lib/devshield/quarantine', help='Root-owned 0700 quarantine directory')
42
+ parser.add_argument('--max-files', type=int, help='ClamAV maximum archive member count')
43
+ parser.add_argument('--max-filesize-mb', type=int, help='ClamAV per-file limit in MB (default: engine setting)')
44
+ parser.add_argument('--max-scansize-mb', type=int, help='ClamAV archive scan limit in MB (default: engine setting)')
20
45
  parser.add_argument('--host-root', default='/', help='Host directory to scan (default /)')
21
46
  parser.add_argument('--skip-host', action='store_true')
22
47
  parser.add_argument('--skip-docker', action='store_true')
@@ -28,6 +53,9 @@ def main():
28
53
  parser.error('Run this script on Linux.')
29
54
  if args.heartbeat < 1:
30
55
  parser.error('--heartbeat must be positive')
56
+ for value in (args.max_filesize_mb, args.max_scansize_mb, args.max_files):
57
+ if value is not None and value < 1:
58
+ parser.error('Scan limits must be positive')
31
59
  if not shutil.which('clamscan'):
32
60
  parser.error('clamscan is missing. Install ClamAV and update its databases first; see README.md.')
33
61
  report_dir = Path(args.reports).resolve() / (dt.datetime.now(dt.timezone.utc).strftime('%Y%m%dT%H%M%SZ') + '-' + str(os.getpid()))
@@ -81,12 +109,23 @@ def main():
81
109
  scratch = tempfile.TemporaryDirectory(prefix='devshield-', dir=args.temp_dir)
82
110
  def scan(path, label, excludes=()):
83
111
  command = ['clamscan', '--recursive=yes', '--infected', '--verbose', '--scan-archive=yes',
84
- '--cross-fs=yes', '--follow-dir-symlinks=0', '--follow-file-symlinks=0']
112
+ '--cross-fs=yes', '--follow-dir-symlinks=0', '--follow-file-symlinks=0', '--alert-exceeds-max=yes']
113
+ if args.max_filesize_mb is not None:
114
+ command.append(f'--max-filesize={args.max_filesize_mb}M')
115
+ if args.max_scansize_mb is not None:
116
+ command.append(f'--max-scansize={args.max_scansize_mb}M')
117
+ if args.max_files is not None:
118
+ command.append(f'--max-files={args.max_files}')
85
119
  command.extend('--exclude-dir=^' + re.escape(str(Path(p).resolve())) + r'(/|$)' for p in excludes)
86
120
  rc, log = run([*command, '--', str(path)], label)
121
+ warnings = coverage_warnings(log)
87
122
  report['scans'].append({'target': str(path), 'label': label, 'exit_code': rc, 'log': log,
88
- 'result': {0: 'No signature detections reported', 1: 'Detection reported'}.get(rc, 'Scan error')})
123
+ 'coverage_warnings': warnings,
124
+ 'result': ('Incomplete coverage; review log' if warnings else
125
+ {0: 'No signature detections reported', 1: 'Detection reported'}.get(rc, 'Scan error'))})
89
126
  save()
127
+ for warning in warnings:
128
+ issue(f'{label}: {warning} See {log}')
90
129
  if rc not in (0, 1):
91
130
  issue(f'{label}: ClamAV failed; see {log}')
92
131
  save()
@@ -96,12 +135,14 @@ def main():
96
135
  version = subprocess.run(['clamscan', '--version'], capture_output=True, text=True)
97
136
  report['engine'] = version.stdout.strip()
98
137
  save()
138
+ if args.remediate_running:
139
+ inspect_running(run, issue, save, report, scratch.name, args.quarantine, coverage_warnings)
99
140
  if not args.skip_host:
100
141
  if not Path(args.host_root).is_dir():
101
142
  issue('Host root is not a directory: ' + args.host_root)
102
143
  else:
103
144
  scan(Path(args.host_root).resolve(), 'Host filesystem',
104
- ['/proc', '/sys', '/dev', '/run', '/var/lib/docker', '/var/lib/containerd', report_dir.parent, scratch.name])
145
+ ['/proc', '/sys', '/dev', '/run', '/var/lib/docker', '/var/lib/containerd', report_dir.parent, scratch.name, args.quarantine])
105
146
  if not args.skip_docker:
106
147
  if not shutil.which('docker'):
107
148
  issue('Docker CLI missing; containers and volumes were not inspected.')
@@ -143,7 +184,7 @@ def main():
143
184
  issue(f'Volume {name}: {exc}')
144
185
  for source in sorted(mounts):
145
186
  if Path(source).exists():
146
- scan(source, 'Docker mount ' + source, ['/proc', '/sys', '/dev', '/run', report_dir.parent, scratch.name])
187
+ scan(source, 'Docker mount ' + source, ['/proc', '/sys', '/dev', '/run', report_dir.parent, scratch.name, args.quarantine])
147
188
  else:
148
189
  issue('Docker mount source inaccessible: ' + source)
149
190
  except (OSError, RuntimeError, ValueError, KeyError, subprocess.SubprocessError) as exc:
@@ -159,7 +200,7 @@ def main():
159
200
  print(f'Report: {report_dir / "report.json"}', flush=True)
160
201
  if report['issues']:
161
202
  return 2
162
- return 1 if any(s['exit_code'] == 1 for s in report['scans']) else 0
203
+ return 1 if report.get('process_response') or any(s['exit_code'] == 1 for s in report['scans']) else 0
163
204
 
164
205
 
165
206
  if __name__ == '__main__':
@@ -0,0 +1,169 @@
1
+ """Conservative process-executable response; never infer a verdict from a PID/name."""
2
+ import hashlib
3
+ import os
4
+ from pathlib import Path
5
+ import re
6
+ import signal
7
+ import stat
8
+
9
+
10
+ def signature_names(log_path):
11
+ names = []
12
+ with open(log_path, encoding='utf-8', errors='replace') as stream:
13
+ for line in stream:
14
+ match = re.search(r': ([^\r\n]+) FOUND\s*$', line)
15
+ if match:
16
+ name = match.group(1)
17
+ # Coverage, heuristic/PUA and test alerts are insufficient for automatic response.
18
+ if not name.startswith(('Heuristics.', 'PUA.')) and not re.search(r'eicar|test[.-]signature', name, re.I):
19
+ names.append(name)
20
+ return names
21
+
22
+
23
+ def digest(stream):
24
+ stream.seek(0)
25
+ result = hashlib.sha256()
26
+ for block in iter(lambda: stream.read(1024 * 1024), b''):
27
+ result.update(block)
28
+ return result.hexdigest()
29
+
30
+
31
+ def protected_pids():
32
+ protected = {1}
33
+ pid = os.getpid()
34
+ while pid > 0 and pid not in protected:
35
+ protected.add(pid)
36
+ try:
37
+ text = Path(f'/proc/{pid}/stat').read_text()
38
+ pid = int(text[text.rfind(')') + 2:].split()[1])
39
+ except (OSError, ValueError, IndexError):
40
+ break
41
+ return protected
42
+
43
+
44
+ def move_original(source, before, destination):
45
+ """Same-filesystem rename only; never copy-and-delete across devices."""
46
+ parentfd = os.open(source.parent, os.O_RDONLY | os.O_DIRECTORY)
47
+ try:
48
+ current = os.stat(source.name, dir_fd=parentfd, follow_symlinks=False)
49
+ if not stat.S_ISREG(current.st_mode) or (current.st_dev, current.st_ino) != (before.st_dev, before.st_ino):
50
+ raise RuntimeError('Original path changed; not moved')
51
+ if destination.exists():
52
+ raise RuntimeError('Original quarantine destination already exists')
53
+ os.rename(source.name, str(destination), src_dir_fd=parentfd)
54
+ os.chmod(destination, 0o600)
55
+ finally:
56
+ os.close(parentfd)
57
+
58
+
59
+ def inspect_running(run, issue, save, report, scratch, quarantine, coverage_warnings):
60
+ """Scan immutable copies, pin process identities, preserve evidence before SIGKILL."""
61
+ if os.geteuid() != 0:
62
+ issue('Running-process response requires root; stage skipped.')
63
+ return
64
+ if not hasattr(os, 'pidfd_open') or not hasattr(signal, 'pidfd_send_signal'):
65
+ issue('Running-process response requires Python/Linux pidfd support; no unsafe PID-only fallback is used.')
66
+ return
67
+ quarantine = Path(quarantine).absolute()
68
+ quarantine.mkdir(parents=True, mode=0o700, exist_ok=True)
69
+ metadata = quarantine.lstat()
70
+ if not stat.S_ISDIR(metadata.st_mode) or metadata.st_uid != 0 or metadata.st_mode & 0o077:
71
+ issue('Quarantine must be a root-owned directory with mode 0700; stage skipped.')
72
+ return
73
+ protected = protected_pids()
74
+ results = report.setdefault('process_response', [])
75
+ cache = {}
76
+ host_namespace = os.readlink('/proc/self/ns/mnt')
77
+ pids = sorted(int(p.name) for p in Path('/proc').iterdir() if p.name.isdigit())
78
+ print(f'[Processes] Inspecting executables for {len(pids)} processes. Scripts, RAM and persistence are not covered.', flush=True)
79
+ for number, pid in enumerate(pids, 1):
80
+ if pid in protected:
81
+ continue
82
+ pidfd = None
83
+ try:
84
+ pidfd = os.pidfd_open(pid)
85
+ exe_link = Path(f'/proc/{pid}/exe')
86
+ with exe_link.open('rb') as executable:
87
+ before = os.fstat(executable.fileno())
88
+ if not stat.S_ISREG(before.st_mode):
89
+ continue
90
+ key = (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns, before.st_ctime_ns)
91
+ source_name = os.readlink(exe_link)
92
+ print(f'[Processes {number}/{len(pids)}] PID {pid}: {source_name}', flush=True)
93
+ if key not in cache:
94
+ snapshot = Path(scratch) / f'process-{pid}.bin'
95
+ with snapshot.open('xb') as target:
96
+ for block in iter(lambda: executable.read(1024 * 1024), b''):
97
+ target.write(block)
98
+ with snapshot.open('rb') as target:
99
+ snapshot_hash = digest(target)
100
+ rc, log = run(['clamscan', '--infected', '--alert-exceeds-max=yes', '--', str(snapshot)], f'Process executable PID {pid}')
101
+ for warning in coverage_warnings(log):
102
+ issue(f'PID {pid}: {warning} See {log}')
103
+ names = signature_names(log) if rc == 1 else []
104
+ # Never kill when the executable changed while its snapshot was scanned.
105
+ if digest(executable) != snapshot_hash:
106
+ issue(f'PID {pid}: executable changed during inspection; response skipped.')
107
+ snapshot.unlink()
108
+ continue
109
+ if rc not in (0, 1):
110
+ issue(f'PID {pid}: executable scan failed; see {log}')
111
+ if rc == 1 and not names:
112
+ issue(f'PID {pid}: non-actionable alert; no process killed; see {log}')
113
+ if names:
114
+ evidence = quarantine / (snapshot_hash + '.sample')
115
+ # Create-only: never overwrite a prior sample or follow a pre-existing symlink.
116
+ try:
117
+ with evidence.open('xb') as target, snapshot.open('rb') as original:
118
+ os.chmod(evidence, 0o600)
119
+ for block in iter(lambda: original.read(1024 * 1024), b''):
120
+ target.write(block)
121
+ except FileExistsError:
122
+ if evidence.is_symlink():
123
+ raise RuntimeError('Unsafe quarantine sample link')
124
+ with evidence.open('rb') as existing:
125
+ if digest(existing) != snapshot_hash:
126
+ raise RuntimeError('Quarantine sample hash mismatch')
127
+ cache[key] = (names, snapshot_hash, log, str(evidence))
128
+ else:
129
+ cache[key] = ([], snapshot_hash, log, None)
130
+ snapshot.unlink()
131
+ names, snapshot_hash, log, evidence = cache[key]
132
+ if not names:
133
+ continue
134
+ current = exe_link.stat()
135
+ if (current.st_dev, current.st_ino) != (before.st_dev, before.st_ino) or digest(executable) != snapshot_hash:
136
+ issue(f'PID {pid}: executable identity changed; response skipped.')
137
+ continue
138
+ action = {'pid': pid, 'executable': source_name, 'signatures': names, 'sha256': snapshot_hash,
139
+ 'log': log, 'evidence': evidence, 'termination': 'pending', 'file_action': 'not moved'}
140
+ results.append(action)
141
+ container_namespace = os.readlink(f'/proc/{pid}/ns/mnt') != host_namespace
142
+ save() # Preserve evidence and intent before touching the process.
143
+ signal.pidfd_send_signal(pidfd, signal.SIGKILL)
144
+ action['termination'] = 'SIGKILL sent via pidfd'
145
+ print(f'[RESPONSE] PID {pid}: SIGKILL sent; signatures: {names}', flush=True)
146
+ save()
147
+ # Never resolve a container path against the host filesystem.
148
+ if container_namespace:
149
+ action['file_action'] = 'container namespace: evidence saved, original not moved; rebuild container'
150
+ issue(f'PID {pid}: container executable requires container rebuild; supervisors may restart it.')
151
+ elif source_name.endswith(' (deleted)'):
152
+ action['file_action'] = 'original already unlinked; evidence retained'
153
+ else:
154
+ source = Path(source_name)
155
+ # Pin the parent directory to avoid a changed parent/symlink redirect.
156
+ destination = quarantine / f'{snapshot_hash}-{pid}.original'
157
+ move_original(source, before, destination)
158
+ action['file_action'] = 'moved to ' + str(destination)
159
+ save()
160
+ except (FileNotFoundError, ProcessLookupError):
161
+ # Most often a process exited between enumeration and inspection.
162
+ if 'action' in locals() and action.get('pid') == pid:
163
+ action['file_action'] = 'process/path exited during response; inspect source manually'
164
+ issue(f'PID {pid}: process/path disappeared during response; original removal unverified.')
165
+ except (OSError, RuntimeError) as exc:
166
+ issue(f'PID {pid}: {exc}; consult process_response for actions already performed.')
167
+ finally:
168
+ if pidfd is not None:
169
+ os.close(pidfd)
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: devshield-scanner
3
- Version: 0.1.0
4
- Summary: Read-only Linux host and Docker filesystem inspection using ClamAV
3
+ Version: 0.2.0
4
+ Summary: Linux host/Docker inspection with opt-in response for detected process executables
5
5
  License-Expression: MIT
6
6
  Classifier: Development Status :: 3 - Alpha
7
7
  Classifier: Environment :: Console
@@ -14,7 +14,17 @@ Dynamic: license-file
14
14
 
15
15
  # DevShield for Linux
16
16
 
17
- An installable Python CLI package is included. See **INSTALL.md** to build a wheel and install the `devshield` command on servers using an isolated environment under `/opt/devshield`. There is no published npm or Python registry package yet. `devshield --version` displays the CLI version; existing scanner flags are unchanged.
17
+ ## Process-first response (0.2.0)
18
+
19
+ `sudo /opt/devshield/bin/devshield --remediate-running` first snapshots and scans running executables, sends SIGKILL to signature-detected processes, and attempts to move matching host executables into `/var/lib/devshield/quarantine`. It then scans host files and Docker data. Without this flag, scanning remains read-only. This is an experimental opt-in response mode and can interrupt production services. Process response currently requires root and Python/Linux pidfd support (normally Python 3.9+ and Linux 5.3+).
20
+
21
+ Heuristic, PUA, encrypted-content, scan-limit and EICAR test alerts never trigger automatic process response. Evidence copies and action records are saved before termination. PID 1, DevShield and its ancestors are excluded. Original-file moves require a matching inode and a same-filesystem rename; failures are reported rather than falling back to destructive copy/delete. Container executables are not moved: rebuild affected containers from trusted images. Supervisors may restart terminated processes. Script interpreters, injected code, memory-only malware and persistence are not comprehensively inspected, and files found during the subsequent host/container scan are not automatically removed. Quarantine samples must not be executed. Quarantine is excluded from later scans.
22
+
23
+ `--max-files 200000` raises the archive member-count limit when needed. Example for the large container export discussed during validation: `devshield --remediate-running --max-files 200000 --max-filesize-mb 1536 --max-scansize-mb 2048`. Limits do not apply to process snapshots, which retain ClamAV defaults with limit alerts. Increasing limits raises resource use. A privileged attacker can still evade or interfere with live-host inspection. There is no guarantee of complete malware removal.
24
+
25
+ An installable Python CLI package is included. The Python distribution is `devshield-scanner`; the command is `devshield`. See **INSTALL.md** for isolated installation under `/opt/devshield`. `devshield --version` displays the CLI version.
26
+
27
+ Version 0.1.1 enables ClamAV scan-limit alerts and marks zero-data scans, missing summaries and logged warnings/errors as incomplete coverage (exit 2), even when ClamAV returns 0. Limit alerts are not proof of malware. Optional `--max-filesize-mb` and `--max-scansize-mb` override engine limits in MB. Increasing limits can increase memory, CPU and runtime; engine hard limits still apply. A positive data count alone does not prove every archive entry was inspected.
18
28
 
19
29
  Read-only host and Docker filesystem malware scanning using the locally installed ClamAV signature engine. Requires Linux, Python 3.8+, `clamscan`, updated ClamAV databases, and the Docker CLI for container scanning. Run on the Docker host with a local Unix socket context. Root access normally provides the required file/socket access.
20
30
 
@@ -1,6 +1,7 @@
1
1
  LICENSE
2
2
  README.md
3
3
  devshield_linux.py
4
+ devshield_processes.py
4
5
  pyproject.toml
5
6
  devshield_scanner.egg-info/PKG-INFO
6
7
  devshield_scanner.egg-info/SOURCES.txt
@@ -0,0 +1,2 @@
1
+ devshield_linux
2
+ devshield_processes
@@ -4,8 +4,8 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "devshield-scanner"
7
- version = "0.1.0"
8
- description = "Read-only Linux host and Docker filesystem inspection using ClamAV"
7
+ version = "0.2.0"
8
+ description = "Linux host/Docker inspection with opt-in response for detected process executables"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
11
11
  dependencies = []
@@ -22,4 +22,4 @@ classifiers = [
22
22
  devshield = "devshield_linux:main"
23
23
 
24
24
  [tool.setuptools]
25
- py-modules = ["devshield_linux"]
25
+ py-modules = ["devshield_linux", "devshield_processes"]
@@ -1 +0,0 @@
1
- devshield_linux