-
Notifications
You must be signed in to change notification settings - Fork 6
Add containerized shell toolbox #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
0892143
04d652d
6307ae6
c49cf62
b8aafc6
7f2dbdb
1245c0d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| #!/bin/bash | ||
| # SPDX-FileCopyrightText: GitHub, Inc. | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| # Build seclab container shell images. | ||
| # Must be run from the root of the seclab-taskflows repository. | ||
| # Images must be rebuilt whenever a Dockerfile changes. | ||
| # | ||
| # Usage: ./scripts/build_container_images.sh [base|malware|network|sast|all] | ||
| # default: all | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| __root="$(cd "${__dir}/.." && pwd)" | ||
| CONTAINERS_DIR="${__root}/src/seclab_taskflows/containers" | ||
|
|
||
| build_base() { | ||
| echo "Building seclab-shell-base..." | ||
| docker build -t seclab-shell-base:latest "${CONTAINERS_DIR}/base/" | ||
| } | ||
|
|
||
| build_malware() { | ||
| echo "Building seclab-shell-malware-analysis..." | ||
| docker build -t seclab-shell-malware-analysis:latest "${CONTAINERS_DIR}/malware_analysis/" | ||
| } | ||
|
|
||
| build_network() { | ||
| echo "Building seclab-shell-network-analysis..." | ||
| docker build -t seclab-shell-network-analysis:latest "${CONTAINERS_DIR}/network_analysis/" | ||
| } | ||
|
|
||
| build_sast() { | ||
| echo "Building seclab-shell-sast..." | ||
| docker build -t seclab-shell-sast:latest "${CONTAINERS_DIR}/sast/" | ||
| } | ||
|
|
||
| target="${1:-all}" | ||
|
|
||
| case "$target" in | ||
| base) | ||
| build_base | ||
| ;; | ||
| malware) | ||
| build_base | ||
| build_malware | ||
| ;; | ||
| network) | ||
| build_base | ||
| build_network | ||
| ;; | ||
| sast) | ||
| build_base | ||
| build_sast | ||
| ;; | ||
| all) | ||
| build_base | ||
| build_malware | ||
| build_network | ||
| build_sast | ||
| ;; | ||
| *) | ||
| echo "Unknown target: $target" >&2 | ||
| echo "Usage: $0 [base|malware|network|sast|all]" >&2 | ||
| exit 1 | ||
| ;; | ||
| esac | ||
|
|
||
| echo "Done." |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,127 @@ | ||
| #!/bin/bash | ||
| # SPDX-FileCopyrightText: GitHub, Inc. | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| # Run container shell demo taskflows. | ||
| # Must be run from the root of the seclab-taskflows repository. | ||
| # | ||
| # Usage: | ||
| # ./scripts/run_container_shell_demo.sh base [workspace_dir] | ||
| # ./scripts/run_container_shell_demo.sh malware [workspace_dir] [target_filename] | ||
| # ./scripts/run_container_shell_demo.sh network [workspace_dir] [capture_filename] | ||
| # ./scripts/run_container_shell_demo.sh sast [workspace_dir] [target] | ||
| # | ||
| # If workspace_dir is omitted a temporary directory is used. | ||
| # Requires AI_API_TOKEN to be set in the environment. | ||
|
|
||
| set -euo pipefail | ||
|
|
||
| __dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" | ||
| __root="$(cd "${__dir}/.." && pwd)" | ||
|
|
||
| export PATH="${__root}/.venv/bin:${PATH}" | ||
|
|
||
| if [ -z "${AI_API_TOKEN:-}" ]; then | ||
| echo "AI_API_TOKEN is not set" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| demo="${1:-}" | ||
| if [ -z "$demo" ]; then | ||
| echo "Usage: $0 <base|malware|network|sast> [workspace_dir] [target]" >&2 | ||
| exit 1 | ||
| fi | ||
|
|
||
| workspace="${2:-$(mktemp -d)}" | ||
| mkdir -p "$workspace" | ||
|
|
||
| case "$demo" in | ||
| base) | ||
| target="${3:-hello}" | ||
| if [ ! -f "${workspace}/${target}" ]; then | ||
| echo "Copying /bin/ls to ${workspace}/${target} as demo target" | ||
| cp /bin/ls "${workspace}/${target}" | ||
| fi | ||
| CONTAINER_WORKSPACE="$workspace" \ | ||
| LOG_DIR="${__root}/logs" \ | ||
| python -m seclab_taskflow_agent \ | ||
| -t seclab_taskflows.taskflows.container_shell.demo_base \ | ||
| -g target="$target" | ||
| ;; | ||
| malware) | ||
| target="${3:-suspicious.elf}" | ||
| if [ ! -f "${workspace}/${target}" ]; then | ||
| echo "Copying /bin/ls to ${workspace}/${target} as demo target" | ||
| cp /bin/ls "${workspace}/${target}" | ||
| fi | ||
| CONTAINER_WORKSPACE="$workspace" \ | ||
| LOG_DIR="${__root}/logs" \ | ||
| python -m seclab_taskflow_agent \ | ||
| -t seclab_taskflows.taskflows.container_shell.demo_malware_analysis \ | ||
| -g target="$target" | ||
| ;; | ||
| network) | ||
| capture="${3:-sample.pcap}" | ||
| if [ ! -f "${workspace}/${capture}" ]; then | ||
| echo "No pcap found at ${workspace}/${capture}" >&2 | ||
| echo "Provide a pcap file or set workspace_dir to a directory containing one." >&2 | ||
| exit 1 | ||
| fi | ||
| CONTAINER_WORKSPACE="$workspace" \ | ||
| LOG_DIR="${__root}/logs" \ | ||
| python -m seclab_taskflow_agent \ | ||
| -t seclab_taskflows.taskflows.container_shell.demo_network_analysis \ | ||
| -g capture="$capture" | ||
| ;; | ||
| sast) | ||
| target="${3:-.}" | ||
| target_path="${workspace}/${target}" | ||
| if [ "$target" != "." ] && [ ! -d "$target_path" ] && [ ! -f "$target_path" ]; then | ||
| echo "No source found at ${target_path}" >&2 | ||
| echo "Provide a source directory or file in workspace_dir." >&2 | ||
| exit 1 | ||
| fi | ||
| if [ "$target" = "." ] && [ -z "$(ls -A "$workspace" 2>/dev/null)" ]; then | ||
| echo "Generating demo Python source in ${workspace}" | ||
| cat > "${workspace}/demo.py" <<'PYEOF' | ||
| import os | ||
| import subprocess | ||
|
|
||
|
|
||
| def read_config(path): | ||
| with open(path) as f: | ||
| return f.read() | ||
|
|
||
|
|
||
| def run_command(cmd): | ||
| # intentional anti-pattern for demo purposes | ||
| return subprocess.run(cmd, shell=True, capture_output=True, text=True) | ||
|
|
||
|
|
||
| def process_input(user_input): | ||
| result = run_command(f"echo {user_input}") | ||
| return result.stdout | ||
|
|
||
|
|
||
| def main(): | ||
| config = read_config("/etc/demo.conf") if os.path.exists("/etc/demo.conf") else "" | ||
| output = process_input("hello world") | ||
| print(config, output) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| main() | ||
| PYEOF | ||
| target="demo.py" | ||
| fi | ||
| CONTAINER_WORKSPACE="$workspace" \ | ||
| LOG_DIR="${__root}/logs" \ | ||
| python -m seclab_taskflow_agent \ | ||
| -t seclab_taskflows.taskflows.container_shell.demo_sast \ | ||
| -g target="$target" | ||
| ;; | ||
| *) | ||
| echo "Unknown demo: $demo. Choose base, malware, network, or sast." >&2 | ||
| exit 1 | ||
| ;; | ||
| esac |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # SPDX-FileCopyrightText: GitHub, Inc. | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| FROM debian:bookworm-slim | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| bash coreutils python3 python3-pip curl wget git ca-certificates \ | ||
| file binutils xxd \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
| WORKDIR /workspace | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| # SPDX-FileCopyrightText: GitHub, Inc. | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| FROM seclab-shell-base:latest | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
anticomputer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| binwalk yara libimage-exiftool-perl checksec \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
| # radare2 is not in Debian bookworm apt; install prebuilt deb from GitHub releases | ||
| RUN ARCH=$(dpkg --print-architecture) \ | ||
| && R2_TAG=$(curl -fsSL "https://api.github.com/repos/radareorg/radare2/releases/latest" \ | ||
| | grep -o '"tag_name": *"[^"]*"' | grep -o '"[^"]*"$' | tr -d '"') \ | ||
| && R2_VER="${R2_TAG#v}" \ | ||
| && curl -fsSL "https://github.com/radareorg/radare2/releases/download/${R2_TAG}/radare2_${R2_VER}_${ARCH}.deb" \ | ||
| -o /tmp/r2.deb \ | ||
| && apt-get install -y /tmp/r2.deb \ | ||
| && rm /tmp/r2.deb | ||
| RUN pip3 install --no-cache-dir --break-system-packages pwntools capstone volatility3 | ||
anticomputer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| # SPDX-FileCopyrightText: GitHub, Inc. | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| FROM seclab-shell-base:latest | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
anticomputer marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| nmap tcpdump tshark netcat-openbsd dnsutils curl jq httpie \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| # SPDX-FileCopyrightText: GitHub, Inc. | ||
| # SPDX-License-Identifier: MIT | ||
|
|
||
| FROM seclab-shell-base:latest | ||
| RUN apt-get update && apt-get install -y --no-install-recommends \ | ||
| universal-ctags global cscope ripgrep fd-find graphviz tree \ | ||
| && ln -s /usr/bin/fdfind /usr/local/bin/fd \ | ||
| && rm -rf /var/lib/apt/lists/* | ||
| RUN pip3 install --no-cache-dir --break-system-packages semgrep pyan3 |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,107 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||
| # SPDX-FileCopyrightText: GitHub, Inc. | ||||||||||||||||||||||||||||||||||||||||||||||||
| # SPDX-License-Identifier: MIT | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import atexit | ||||||||||||||||||||||||||||||||||||||||||||||||
| import logging | ||||||||||||||||||||||||||||||||||||||||||||||||
| import os | ||||||||||||||||||||||||||||||||||||||||||||||||
anticomputer marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
| import subprocess | ||||||||||||||||||||||||||||||||||||||||||||||||
| import uuid | ||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import Annotated | ||||||||||||||||||||||||||||||||||||||||||||||||
anticomputer marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| from fastmcp import FastMCP | ||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import Field | ||||||||||||||||||||||||||||||||||||||||||||||||
| from seclab_taskflow_agent.path_utils import log_file_name | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| logging.basicConfig( | ||||||||||||||||||||||||||||||||||||||||||||||||
| level=logging.DEBUG, | ||||||||||||||||||||||||||||||||||||||||||||||||
| format="%(asctime)s - %(levelname)s - %(message)s", | ||||||||||||||||||||||||||||||||||||||||||||||||
| filename=log_file_name("container_shell.log"), | ||||||||||||||||||||||||||||||||||||||||||||||||
| filemode="a", | ||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| mcp = FastMCP("ContainerShell") | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| _container_name: str | None = None | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
| CONTAINER_IMAGE = os.environ.get("CONTAINER_IMAGE", "") | ||||||||||||||||||||||||||||||||||||||||||||||||
| CONTAINER_WORKSPACE = os.environ.get("CONTAINER_WORKSPACE", "") | ||||||||||||||||||||||||||||||||||||||||||||||||
| CONTAINER_TIMEOUT = int(os.environ.get("CONTAINER_TIMEOUT", "30")) | ||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||
anticomputer marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+28
to
+29
|
||||||||||||||||||||||||||||||||||||||||||||||||
| CONTAINER_TIMEOUT = int(os.environ.get("CONTAINER_TIMEOUT", "30")) | |
| def _get_container_timeout(default: int = 30) -> int: | |
| """Parse CONTAINER_TIMEOUT from the environment defensively.""" | |
| raw_value = os.environ.get("CONTAINER_TIMEOUT") | |
| if not raw_value: | |
| return default | |
| try: | |
| timeout = int(raw_value) | |
| if timeout <= 0: | |
| raise ValueError | |
| except ValueError: | |
| logging.warning( | |
| "Invalid CONTAINER_TIMEOUT value %r; falling back to default %ss", | |
| raw_value, | |
| default, | |
| ) | |
| return default | |
| return timeout | |
| CONTAINER_TIMEOUT = _get_container_timeout() |
anticomputer marked this conversation as resolved.
Show resolved
Hide resolved
anticomputer marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Mar 4, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
docker run is executed via subprocess.run(...) without handling FileNotFoundError/OSError (docker missing) or TimeoutExpired. Either case will currently raise and crash the server/tool call. Wrap this call in try/except and surface a clear RuntimeError (or a user-facing error string from shell_exec) so failures are reported cleanly.
This issue also appears in the following locations of the same file:
- line 60
- line 92
Uh oh!
There was an error while loading. Please reload this page.