Skip to content

Commit c16136b

Browse files
author
antitree
committed
feat: Adding a new cli tool. Expanding the UI elements to be more intuitive. Rebuilt the system call diff logic so that it shows which ones are most dangerous
1 parent ed413ab commit c16136b

File tree

11 files changed

+382
-153
lines changed

11 files changed

+382
-153
lines changed

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,4 @@ EXPOSE 5000
3535
RUN apt-get purge -y gcc && apt-get autoremove -y && rm -rf /var/lib/apt/lists/*
3636

3737
# Command to run the Flask app
38-
CMD ["flask", "run"]
38+
CMD ["flask", "run", "--debug"]

common/diff.py

Lines changed: 123 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,42 @@
44
from rich.console import Console
55
from rich import box
66
from rich.text import Text
7+
from rich.style import Style
8+
9+
10+
DANGEROUS_SYSCALLS = ["acct", "add_key", "bpf", "clock_adjtime", "clone", "create_module",
11+
"delete_module", "finit_module", "get_kernel_syms", "get_mempolicy",
12+
"init_module", "ioperm", "iopl", "kcmp", "kexec_file_load", "kexec_load",
13+
"keyctl", "lookup_dcookie", "mbind", "mount", "move_pages", "nfsservctl",
14+
"open_by_handle_at", "perf_event_open", "personality", "pivot_root",
15+
"process_vm_readv", "process_vm_writev", "ptrace", "query_module",
16+
"quotactl", "reboot", "request_key", "set_mempolicy", "setns",
17+
"settimeofday", "stime", "swapon", "swapoff", "sysfs", "_sysctl", "umount",
18+
"umount2", "unshare", "uselib", "userfaultfd", "ustat", "vm86", "vm86old"]
19+
20+
def reduce_action(action):
21+
ALLOW = ["ALLOW", action, "permissive"]
22+
DENY = ["DENY", action, "restrictive"]
23+
CONDITION = ["CONDITION", action, "restrictive"]
24+
UNKNOWN = ["ERROR-NOT-FOUND", action, "permissive"]
25+
ACTION_MAP = {
26+
"ALLOW": ALLOW,
27+
"ERRNO": DENY,
28+
"ALLOW/ERRORNO": CONDITION,
29+
"N/A": ALLOW,
30+
"LOG": ALLOW,
31+
"KILL": DENY,
32+
"TRACE": CONDITION,
33+
"TRAP": CONDITION,
34+
"Unknown": UNKNOWN,
35+
}
36+
# Check error numbers
37+
if action.startswith("ERRNO"):
38+
return DENY
39+
elif "/" in action and action != "N/A":
40+
return CONDITION
41+
return ACTION_MAP.get(action, f"ERROR MAPPING EFFECTIVE PERMISSIONS {action}")
42+
743

844
def is_convertible_to_int(s):
945
"""Check if a string can be safely converted to an integer."""
@@ -13,66 +49,51 @@ def is_convertible_to_int(s):
1349
except ValueError:
1450
return False
1551

16-
dangerous_syscalls = [
17-
"acct", "add_key", "bpf", "clock_adjtime", "clone", "create_module",
18-
"delete_module", "finit_module", "get_kernel_syms", "get_mempolicy",
19-
"init_module", "ioperm", "iopl", "kcmp", "kexec_file_load", "kexec_load",
20-
"keyctl", "lookup_dcookie", "mbind", "mount", "move_pages", "nfsservctl",
21-
"open_by_handle_at", "perf_event_open", "personality", "pivot_root",
22-
"process_vm_readv", "process_vm_writev", "ptrace", "query_module",
23-
"quotactl", "reboot", "request_key", "set_mempolicy", "setns",
24-
"settimeofday", "stime", "swapon", "swapoff", "sysfs", "_sysctl", "umount",
25-
"umount2", "unshare", "uselib", "userfaultfd", "ustat", "vm86", "vm86old"
26-
]
27-
28-
def get_seccomp_policy(container1):
29-
full1, d1 = get_seccomp_filters(container1["pid"])
30-
if d1:
31-
container1["summary"] = d1.syscallSummary
32-
else:
33-
container1["summary"] = {}
34-
35-
da1 = d1.defaultAction
36-
37-
console = Console()
38-
table = Table(show_header=True, show_lines=True, box=box.HEAVY_EDGE, style="green", pad_edge=False)
39-
table.add_column(header="Container", justify="left", min_width=20)
40-
table.add_column(header=container1["name"], justify="left", max_width=20, overflow=None)
41-
table.add_column(header=container2["name"], justify="left", max_width=20, overflow=None)
42-
43-
# Add Seccomp and Capabilities Information
44-
table.add_custom_row("[b]seccomp", container1["seccomp"])
45-
table.add_custom_row("[b]caps", container1["caps"], end_section=True)
46-
47-
# Iterate through the global SYSCALLS dict
48-
for syscall_num, syscall_info in SYSCALLS.items():
49-
syscall_name = syscall_info[1]
50-
51-
# Determine effective policy for container1
52-
if syscall_name in container1["summary"]:
53-
action1 = container1["summary"][syscall_name].get("action", da1)
54-
count1 = container1["summary"][syscall_name].get("count", 0)
55-
effective_policy1 = f"{action1}"
52+
def get_seccomp_policy(container):
53+
"""Get the seccomp policy and return a detailed table for every syscall."""
54+
try:
55+
full, d = get_seccomp_filters(container["pid"])
56+
if d:
57+
container["summary"] = d.syscallSummary
5658
else:
57-
effective_policy1 = f"{da1}"
59+
container["summary"] = {}
5860

59-
table.add_custom_row(syscall_name, effective_policy1)
61+
default_action = d.defaultAction if d else "unknown"
6062

61-
# Add total instructions row
62-
container1["total"] = container1["summary"].get("total", {"count": 0}).get("count")
63-
table.add_custom_row("Total Instructions", str(container1["total"]))
63+
console = Console()
64+
table = Table(show_header=True, show_lines=True, box=box.HEAVY_EDGE, style="green", pad_edge=False)
65+
table.add_column(header="Syscall", justify="left", min_width=20)
66+
table.add_column(header=f"{container['name']} Action", justify="left", min_width=20)
67+
68+
# Iterate through all syscalls in SYSCALLS
69+
for syscall_num, syscall_info in SYSCALLS.items():
70+
syscall_name = syscall_info[1]
71+
72+
# Get the action for this syscall from the container's summary, or default to the default action
73+
if syscall_name in container["summary"]:
74+
action = container["summary"][syscall_name].get("action", default_action)
75+
else:
76+
action = default_action
77+
78+
table.add_custom_row(syscall_name, action)
79+
80+
return table, full
81+
82+
except Exception as e:
83+
console.print(f"An error occurred: {e}", style="bold red")
84+
return None, None
85+
86+
def compare_seccomp_policies(container1, container2, reduce=True, only_diff=True, only_dangerous=False):
87+
"""Compare the seccomp policies of two containers and return a detailed table."""
6488

65-
return table, full1
89+
danger_style = Style(color="red", blink=True, bold=True)
6690

67-
def compare_seccomp_policies(container1, container2, full=False):
91+
6892
try:
69-
# Extract SeccompSummary for both PIDs
7093
full1, d1 = get_seccomp_filters(container1["pid"])
7194
full2, d2 = get_seccomp_filters(container2["pid"])
72-
73-
7495

75-
if d1:
96+
if d1:
7697
container1["summary"] = d1.syscallSummary
7798
else:
7899
container1["summary"] = {}
@@ -82,54 +103,69 @@ def compare_seccomp_policies(container1, container2, full=False):
82103
else:
83104
container2["summary"] = {}
84105

85-
da1 = d1.defaultAction
86-
da2 = d2.defaultAction
106+
default_action1 = d1.defaultAction if d1 else "unknown"
107+
default_action2 = d2.defaultAction if d2 else "unknown"
87108

88109
console = Console()
89110
table = Table(show_header=True, show_lines=True, box=box.HEAVY_EDGE, style="green", pad_edge=False)
90-
table.add_column(header="Container", justify="left", min_width=20)
91-
table.add_column(header=container1["name"], justify="left", max_width=20, overflow=None)
92-
table.add_column(header=container2["name"], justify="left", max_width=20, overflow=None)
93-
111+
table.add_column(header="Syscall", justify="left", min_width=20)
112+
table.add_column(header=f"{container1['name']} Action", justify="left", min_width=20)
113+
table.add_column(header=f"{container2['name']} Action", justify="left", min_width=20)
114+
94115
# Add Seccomp and Capabilities Information
95-
table.add_custom_row("[b]seccomp", container1["seccomp"], container2["seccomp"])
96-
table.add_custom_row("[b]caps", container1["caps"], container2["caps"], end_section=True)
116+
table.add_custom_row("[b]seccomp", container1["seccomp"])
117+
table.add_custom_row("[b]caps", container1["caps"], end_section=True)
118+
97119

98-
# Iterate through the global SYSCALLS dict
120+
# Iterate through all syscalls in SYSCALLS
99121
for syscall_num, syscall_info in SYSCALLS.items():
100122
syscall_name = syscall_info[1]
123+
124+
if only_dangerous and not syscall_name in DANGEROUS_SYSCALLS:
125+
continue
126+
127+
101128

102-
# Determine effective policy for container1
129+
# Get the action for container1
103130
if syscall_name in container1["summary"]:
104-
action1 = container1["summary"][syscall_name].get("action", da1)
105-
count1 = container1["summary"][syscall_name].get("count", 0)
106-
effective_policy1 = f"{action1}"
131+
action1 = container1["summary"][syscall_name].get("action", default_action1)
107132
else:
108-
effective_policy1 = f"{da1}"
133+
action1 = default_action1
109134

110-
# Determine effective policy for container2
135+
# Get the action for container2
111136
if syscall_name in container2["summary"]:
112-
action2 = container2["summary"][syscall_name].get("action", da2)
113-
count2 = container2["summary"][syscall_name].get("count", 0)
114-
effective_policy2 = f"{action2}"
137+
action2 = container2["summary"][syscall_name].get("action", default_action2)
115138
else:
116-
effective_policy2 = f"{da2}"
117-
118-
# Compare effective policies
119-
if effective_policy1 == effective_policy2:
120-
continue # Skip identical policies
121-
122-
table.add_custom_row(syscall_name, effective_policy1, effective_policy2)
123-
124-
# Add total instructions row
125-
container1["total"] = container1["summary"].get("total", {"count": 0}).get("count")
126-
container2["total"] = container2["summary"].get("total", {"count": 0}).get("count")
127-
table.add_custom_row("Total Instructions", str(container1["total"]), str(container2["total"]))
128-
129-
if len(table.rows) <= 3 and container1["total"] == container2["total"]:
130-
console.print(Text("No seccomp filter differences were found between the two containers", justify="center"))
139+
action2 = default_action2
140+
141+
# Reduce the action to an effecctive action of allow or deny
142+
if reduce:
143+
action1 = reduce_action(action1)[0]
144+
action2 = reduce_action(action2)[0]
145+
146+
# Skip identical policies if only_diff is True
147+
if only_diff and action1 == action2:
148+
continue
149+
150+
if syscall_name in DANGEROUS_SYSCALLS:
151+
syscall_name = f":warning:{syscall_name}"
152+
153+
# Add row to table
154+
table.add_custom_row(syscall_name, action1, action2)
131155

132-
return table, full1, full2
156+
except Exception as e:
157+
console.print(f"An error occurred: {e}", style="bold red")
158+
return None, None, None
159+
160+
# Add total instructions row
161+
container1["total"] = container1["summary"].get("total", {"count": 0}).get("count")
162+
container2["total"] = container2["summary"].get("total", {"count": 0}).get("count")
163+
table.add_custom_row("Total Instructions", str(container1["total"]), str(container2["total"]))
164+
if len(table.rows) <= 3 and container1["total"] == container2["total"]:
165+
console.print(Text("No seccomp filter differences were found between the two containers", justify="center"))
166+
167+
return table, full1, full2
133168

134-
except ValueError as e:
135-
print(f"An error occurred: {e}")
169+
170+
171+

common/ptrace.py

Lines changed: 30 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import sys
55
import ctypes
66
import ctypes.util
7-
import argparse
7+
88
from rich.console import Console
99
from rich.table import Table
1010
from rich import box
@@ -131,17 +131,34 @@ def list_seccomp_filters(pid, dump=False, summary=True, allarch=True):
131131

132132
if allarch:
133133
print("Filtering for non-x86-specific instructions is not implemented yet.")
134+
134135

135-
def main():
136-
"""Entry point for the script."""
137-
parser = argparse.ArgumentParser(description="Inspect seccomp profiles for a given PID.")
138-
parser.add_argument("pid", type=int, help="PID of the process to inspect")
139-
parser.add_argument("--dump", action="store_true", help="Dump the raw seccomp filters")
140-
parser.add_argument("--summary", action="store_true", help="Display a summary of the seccomp filters")
141-
parser.add_argument("--allarch", action="store_true", help="Search for all syscalls across any architecture")
142-
143-
args = parser.parse_args()
144-
list_seccomp_filters(args.pid, dump=args.dump, summary=args.summary, allarch=args.allarch)
145136

146-
if __name__ == "__main__":
147-
main()
137+
def list_seccomp_pids():
138+
# Iterate through all the folders in /proc
139+
FILTER = True
140+
141+
console = Console()
142+
table = Table(show_header=False, show_lines=True, box=box.HEAVY_EDGE, style="green", pad_edge=False)
143+
table.add_column(header="PID", justify="left", min_width=20)
144+
table.add_column(header="Seccomp Mode", justify="left", min_width=20)
145+
146+
for pid in os.listdir('/proc'):
147+
if pid.isdigit():
148+
status_path = f"/proc/{pid}/status"
149+
# Check if the status file exists for this PID
150+
cmd = "Unknown"
151+
if os.path.isfile(status_path):
152+
with open(status_path, 'r') as status_file:
153+
for line in status_file:
154+
if line.startswith("Name:"):
155+
cmd = line.split()[1]
156+
if line.startswith("Seccomp_filters:"):
157+
no_instructions = line.split()[1]
158+
if FILTER and int(no_instructions) < 1:
159+
break
160+
else:
161+
# Print the PID and the seccomp value
162+
table.add_row(f"{cmd}({pid})",no_instructions)
163+
break
164+
return table

seccomp_dump.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import common.ptrace as ptrace
2+
import argparse
3+
from rich.console import Console
4+
5+
6+
def main():
7+
"""Entry point for the script."""
8+
parser = argparse.ArgumentParser(description="Inspect seccomp profiles for a given PID.")
9+
parser.add_argument("pid", type=int, nargs="?", help="PID of the process to inspect")
10+
parser.add_argument("--dump", action="store_true", help="Dump the raw seccomp filters")
11+
parser.add_argument("--summary", action="store_true", help="Display a summary of the seccomp filters")
12+
parser.add_argument("--list", action="store_true", help="Display a list of pids with seccomp filters")
13+
parser.add_argument("--allarch", action="store_true", help="Search for all syscalls across any architecture")
14+
15+
args = parser.parse_args()
16+
17+
if not args.list and args.pid is None:
18+
parser.error("PID is required unless --list is specified.")
19+
20+
# If PID is provided and no other options are set, assume --dump
21+
if args.pid is not None and not (args.dump or args.summary or args.list or args.allarch):
22+
args.dump = True
23+
24+
if args.list:
25+
table = ptrace.list_seccomp_pids()
26+
c = Console()
27+
c.print(table)
28+
else:
29+
ptrace.list_seccomp_filters(args.pid, dump=args.dump, summary=args.summary, allarch=args.allarch)
30+
31+
if __name__ == "__main__":
32+
main()

0 commit comments

Comments
 (0)