diff --git a/pyproject.toml b/pyproject.toml
index 4c6bc8f..4461251 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -92,6 +92,7 @@ ignore = [
"PLR1730", # Replace `if` statement with `min()`
"PLR2004", # Magic value used in comparison
"PLW0602", # Using global for variable but no assignment is done
+ "PLW0603", # Using the global statement to update a variable is discouraged
"PLW1508", # Invalid type for environment variable default
"PLW1510", # `subprocess.run` without explicit `check` argument
"RET504", # Unnecessary assignment before `return` statement
@@ -113,3 +114,9 @@ ignore = [
"W291", # Trailing whitespace
"W293", # Blank line contains whitespace
]
+
+[tool.ruff.lint.per-file-ignores]
+"tests/*" = [
+ "S101", # Use of assert (standard in pytest)
+ "SLF001", # Private member accessed (tests legitimately access module internals)
+]
diff --git a/scripts/build_container_images.sh b/scripts/build_container_images.sh
new file mode 100755
index 0000000..d32555d
--- /dev/null
+++ b/scripts/build_container_images.sh
@@ -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."
diff --git a/scripts/run_container_shell_demo.sh b/scripts/run_container_shell_demo.sh
new file mode 100755
index 0000000..a6c43a7
--- /dev/null
+++ b/scripts/run_container_shell_demo.sh
@@ -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 [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
diff --git a/src/seclab_taskflows/containers/base/Dockerfile b/src/seclab_taskflows/containers/base/Dockerfile
new file mode 100644
index 0000000..9a99e3e
--- /dev/null
+++ b/src/seclab_taskflows/containers/base/Dockerfile
@@ -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
diff --git a/src/seclab_taskflows/containers/malware_analysis/Dockerfile b/src/seclab_taskflows/containers/malware_analysis/Dockerfile
new file mode 100644
index 0000000..77afd3d
--- /dev/null
+++ b/src/seclab_taskflows/containers/malware_analysis/Dockerfile
@@ -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 \
+ 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
diff --git a/src/seclab_taskflows/containers/network_analysis/Dockerfile b/src/seclab_taskflows/containers/network_analysis/Dockerfile
new file mode 100644
index 0000000..8c51b8e
--- /dev/null
+++ b/src/seclab_taskflows/containers/network_analysis/Dockerfile
@@ -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 \
+ nmap tcpdump tshark netcat-openbsd dnsutils curl jq httpie \
+ && rm -rf /var/lib/apt/lists/*
diff --git a/src/seclab_taskflows/containers/sast/Dockerfile b/src/seclab_taskflows/containers/sast/Dockerfile
new file mode 100644
index 0000000..32be95a
--- /dev/null
+++ b/src/seclab_taskflows/containers/sast/Dockerfile
@@ -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
diff --git a/src/seclab_taskflows/mcp_servers/container_shell.py b/src/seclab_taskflows/mcp_servers/container_shell.py
new file mode 100644
index 0000000..409f310
--- /dev/null
+++ b/src/seclab_taskflows/mcp_servers/container_shell.py
@@ -0,0 +1,107 @@
+# SPDX-FileCopyrightText: GitHub, Inc.
+# SPDX-License-Identifier: MIT
+
+import atexit
+import logging
+import os
+import subprocess
+import uuid
+from typing import Annotated
+
+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"))
+
+_DEFAULT_WORKDIR = "/workspace"
+
+
+def _start_container() -> str:
+ """Start the Docker container and return its name."""
+ if not CONTAINER_IMAGE:
+ msg = "CONTAINER_IMAGE is not set — cannot start container"
+ raise RuntimeError(msg)
+ if CONTAINER_WORKSPACE and ":" in CONTAINER_WORKSPACE:
+ msg = f"CONTAINER_WORKSPACE must not contain a colon: {CONTAINER_WORKSPACE!r}"
+ raise RuntimeError(msg)
+ name = f"seclab-shell-{uuid.uuid4().hex[:8]}"
+ cmd = ["docker", "run", "-d", "--rm", "--name", name]
+ if CONTAINER_WORKSPACE:
+ cmd += ["-v", f"{CONTAINER_WORKSPACE}:/workspace"]
+ cmd += [CONTAINER_IMAGE, "tail", "-f", "/dev/null"]
+ logging.debug(f"Starting container: {' '.join(cmd)}")
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
+ if result.returncode != 0:
+ msg = f"docker run failed: {result.stderr.strip()}"
+ raise RuntimeError(msg)
+ logging.debug(f"Container started: {name}")
+ return name
+
+
+def _stop_container() -> None:
+ """Stop the running container."""
+ global _container_name
+ if _container_name is None:
+ return
+ logging.debug(f"Stopping container: {_container_name}")
+ result = subprocess.run(
+ ["docker", "stop", "--time", "5", _container_name],
+ capture_output=True,
+ text=True,
+ )
+ if result.returncode != 0:
+ logging.warning(
+ "docker stop failed for container %s: %s",
+ _container_name,
+ result.stderr.strip(),
+ )
+ _container_name = None
+
+
+atexit.register(_stop_container)
+
+
+@mcp.tool()
+def shell_exec(
+ command: Annotated[str, Field(description="Shell command to execute inside the container")],
+ timeout: Annotated[int, Field(description="Timeout in seconds")] = CONTAINER_TIMEOUT,
+ workdir: Annotated[str, Field(description="Working directory inside the container")] = _DEFAULT_WORKDIR,
+) -> str:
+ """Execute a shell command inside the managed Docker container."""
+ global _container_name
+ if _container_name is None:
+ try:
+ _container_name = _start_container()
+ except RuntimeError as e:
+ return f"Failed to start container: {e}"
+
+ cmd = ["docker", "exec", "-w", workdir, _container_name, "bash", "-c", command]
+ logging.debug(f"Executing: {' '.join(cmd)}")
+ try:
+ result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
+ except subprocess.TimeoutExpired:
+ return f"[exit code: timeout after {timeout}s]"
+
+ output = result.stdout
+ if result.stderr:
+ output += result.stderr
+ output += f"[exit code: {result.returncode}]"
+ return output
+
+
+if __name__ == "__main__":
+ mcp.run(show_banner=False)
diff --git a/src/seclab_taskflows/taskflows/container_shell/README.md b/src/seclab_taskflows/taskflows/container_shell/README.md
new file mode 100644
index 0000000..0257538
--- /dev/null
+++ b/src/seclab_taskflows/taskflows/container_shell/README.md
@@ -0,0 +1,133 @@
+# Container Shell Taskflows
+
+Runs arbitrary CLI commands inside an isolated Docker container. One container
+per MCP server process — started on the first `shell_exec` call, stopped on
+exit. An optional host directory is mounted at `/workspace` inside the container.
+
+Four container profiles are provided. Each has its own Dockerfile, toolbox
+YAML, and demo taskflow.
+
+## Profiles
+
+**base** (`seclab-shell-base:latest`)
+General-purpose. Includes bash, coreutils, python3, file, binutils, xxd,
+curl, wget, git.
+
+**malware-analysis** (`seclab-shell-malware-analysis:latest`)
+Static binary and firmware analysis. Extends base with radare2, binwalk,
+yara, exiftool, checksec, capstone, pwntools, volatility3.
+
+**network-analysis** (`seclab-shell-network-analysis:latest`)
+Packet capture analysis and network recon. Extends base with nmap, tcpdump,
+tshark, netcat, dig, jq, httpie.
+
+**sast** (`seclab-shell-sast:latest`)
+Static analysis and code exploration. Extends base with semgrep, pyan3,
+universal-ctags, GNU global, cscope, graphviz, ripgrep, fd, tree.
+
+## Building the images
+
+Run from the repository root:
+
+```
+./scripts/build_container_images.sh
+```
+
+To build a single profile (the base image is always built first when needed):
+
+```
+./scripts/build_container_images.sh base
+./scripts/build_container_images.sh malware
+./scripts/build_container_images.sh network
+./scripts/build_container_images.sh sast
+```
+
+Images only need to be rebuilt when a Dockerfile changes.
+
+## Environment variables
+
+`CONTAINER_WORKSPACE` — host path to mount at `/workspace`. Optional; omit if
+you do not need to pass files into the container.
+
+`CONTAINER_TIMEOUT` — default command timeout in seconds. Defaults to 30 (base
+and network) or 60 (malware analysis and sast).
+
+`LOG_DIR` — where to write `container_shell.log`.
+
+## Running the demos
+
+Create a workspace directory with a target file, then run the agent:
+
+**Base demo** — inspects any ELF binary using standard binutils:
+
+```
+cp /bin/ls /tmp/demo/hello
+CONTAINER_WORKSPACE=/tmp/demo python -m seclab_taskflow_agent \
+ -t seclab_taskflows.taskflows.container_shell.demo_base
+```
+
+**Malware analysis demo** — static triage of a suspicious ELF (not executed):
+
+```
+cp /bin/ls /tmp/samples/suspicious.elf
+CONTAINER_WORKSPACE=/tmp/samples python -m seclab_taskflow_agent \
+ -t seclab_taskflows.taskflows.container_shell.demo_malware_analysis
+```
+
+Override the target filename with `-g target=`.
+
+**Network analysis demo** — analyses a pcap file:
+
+```
+CONTAINER_WORKSPACE=/tmp/captures python -m seclab_taskflow_agent \
+ -t seclab_taskflows.taskflows.container_shell.demo_network_analysis \
+ -g capture=sample.pcap
+```
+
+**SAST demo** — static analysis and call graph extraction for a source repo:
+
+```
+CONTAINER_WORKSPACE=/path/to/src python -m seclab_taskflow_agent \
+ -t seclab_taskflows.taskflows.container_shell.demo_sast
+```
+
+If no source is present the runner script generates a demo Python file with a
+shell-injection anti-pattern so semgrep has something to find:
+
+```
+./scripts/run_container_shell_demo.sh sast
+```
+
+Override the analysis target with `-g target=` (relative to /workspace).
+
+## Using container_shell in your own taskflows
+
+Reference the appropriate toolbox and set `CONTAINER_WORKSPACE` in `env`:
+
+```yaml
+taskflow:
+ - task:
+ agents:
+ - seclab_taskflow_agent.personalities.assistant
+ toolboxes:
+ - seclab_taskflows.toolboxes.container_shell_malware_analysis
+ env:
+ CONTAINER_WORKSPACE: "{{ env('SAMPLE_DIR') }}"
+ user_prompt: |
+ Analyse the binary at /workspace/target.elf using static analysis only.
+```
+
+`shell_exec` requires user confirmation by default (`confirm: [shell_exec]` in
+all toolbox YAMLs). Pass `headless: true` at the task level to skip
+confirmation in automated pipelines.
+
+## Notes
+
+- The container is shared across all `shell_exec` calls within a single
+ taskflow run. State (files written, processes started) persists between calls.
+- `--rm` is set on `docker run`, so the container is removed automatically when
+ stopped.
+- The container name follows the pattern `seclab-shell-<8 hex chars>` and is
+ visible in `docker ps`.
+- If `docker run` fails (e.g. image not found), `shell_exec` returns an error
+ string rather than raising, so the agent can report the problem cleanly.
diff --git a/src/seclab_taskflows/taskflows/container_shell/demo_base.yaml b/src/seclab_taskflows/taskflows/container_shell/demo_base.yaml
new file mode 100644
index 0000000..85e9416
--- /dev/null
+++ b/src/seclab_taskflows/taskflows/container_shell/demo_base.yaml
@@ -0,0 +1,40 @@
+# SPDX-FileCopyrightText: GitHub, Inc.
+# SPDX-License-Identifier: MIT
+
+# Demo: base container shell.
+# Inspects a target binary using tools available in seclab-shell-base.
+# Set CONTAINER_WORKSPACE to a directory containing the binary before running.
+# Example:
+# CONTAINER_WORKSPACE=/tmp/demo python -m seclab_taskflow_agent \
+# -t seclab_taskflows.taskflows.container_shell.demo_base
+
+seclab-taskflow-agent:
+ filetype: taskflow
+ version: "1.0"
+
+globals:
+ target: hello
+
+taskflow:
+ - task:
+ must_complete: true
+ headless: true
+ agents:
+ - seclab_taskflow_agent.personalities.assistant
+ toolboxes:
+ - seclab_taskflows.toolboxes.container_shell_base
+ env:
+ CONTAINER_WORKSPACE: "{{ env('CONTAINER_WORKSPACE') }}"
+ user_prompt: |
+ A binary named {{ globals.target }} is available at /workspace/{{ globals.target }}.
+
+ Run each of the following steps in order and report the findings:
+
+ 1. Confirm the file exists: `ls -lh /workspace/{{ globals.target }}`
+ 2. Identify the file type: `file /workspace/{{ globals.target }}`
+ 3. Extract printable strings (minimum length 8): `strings -n 8 /workspace/{{ globals.target }}`
+ 4. Show the ELF section headers: `readelf -S /workspace/{{ globals.target }}`
+ 5. List exported symbols: `nm -D /workspace/{{ globals.target }} 2>/dev/null || nm /workspace/{{ globals.target }}`
+ 6. Hex dump the first 64 bytes: `xxd /workspace/{{ globals.target }} | head -4`
+
+ Summarise what you learned about the binary from these steps.
diff --git a/src/seclab_taskflows/taskflows/container_shell/demo_malware_analysis.yaml b/src/seclab_taskflows/taskflows/container_shell/demo_malware_analysis.yaml
new file mode 100644
index 0000000..79e53f6
--- /dev/null
+++ b/src/seclab_taskflows/taskflows/container_shell/demo_malware_analysis.yaml
@@ -0,0 +1,65 @@
+# SPDX-FileCopyrightText: GitHub, Inc.
+# SPDX-License-Identifier: MIT
+
+# Demo: malware analysis container shell.
+# Performs static analysis on a suspicious ELF binary placed in CONTAINER_WORKSPACE.
+# The binary is never executed — all analysis is static.
+# Example:
+# CONTAINER_WORKSPACE=/tmp/samples python -m seclab_taskflow_agent \
+# -t seclab_taskflows.taskflows.container_shell.demo_malware_analysis \
+# -g target=suspicious.elf
+
+seclab-taskflow-agent:
+ filetype: taskflow
+ version: "1.0"
+
+globals:
+ target: suspicious.elf
+
+taskflow:
+ - task:
+ must_complete: true
+ headless: true
+ agents:
+ - seclab_taskflow_agent.personalities.assistant
+ toolboxes:
+ - seclab_taskflows.toolboxes.container_shell_malware_analysis
+ env:
+ CONTAINER_WORKSPACE: "{{ env('CONTAINER_WORKSPACE') }}"
+ user_prompt: |
+ Perform static analysis on /workspace/{{ globals.target }}.
+ Do NOT execute the binary at any point.
+
+ Work through each step below and record findings:
+
+ 1. File type and basic info:
+ `file /workspace/{{ globals.target }}`
+
+ 2. Binary metadata (architecture, bits, security mitigations):
+ `rabin2 -I /workspace/{{ globals.target }}`
+
+ 3. Imported library functions:
+ `rabin2 -i /workspace/{{ globals.target }}`
+
+ 4. Exported symbols:
+ `rabin2 -E /workspace/{{ globals.target }}`
+
+ 5. Interesting strings (minimum length 8):
+ `rabin2 -z /workspace/{{ globals.target }}`
+
+ 6. Section layout:
+ `rabin2 -S /workspace/{{ globals.target }}`
+
+ 7. Security mitigations cross-check:
+ `checksec --file=/workspace/{{ globals.target }}`
+
+ 8. Entropy per section (high entropy may indicate packing/encryption):
+ `r2 -q -c "iS~entropy" /workspace/{{ globals.target }} 2>/dev/null || echo "entropy unavailable"`
+
+ Based on the above, assess:
+ - What does the binary appear to do?
+ - Are there indicators of packing, obfuscation, or anti-analysis techniques?
+ - What security mitigations are present or absent?
+ - What imported functions are of interest from a security perspective?
+
+ Write a concise triage report.
diff --git a/src/seclab_taskflows/taskflows/container_shell/demo_network_analysis.yaml b/src/seclab_taskflows/taskflows/container_shell/demo_network_analysis.yaml
new file mode 100644
index 0000000..3241468
--- /dev/null
+++ b/src/seclab_taskflows/taskflows/container_shell/demo_network_analysis.yaml
@@ -0,0 +1,63 @@
+# SPDX-FileCopyrightText: GitHub, Inc.
+# SPDX-License-Identifier: MIT
+
+# Demo: network analysis container shell.
+# Analyses a pcap file placed in CONTAINER_WORKSPACE.
+# Example:
+# CONTAINER_WORKSPACE=/tmp/captures python -m seclab_taskflow_agent \
+# -t seclab_taskflows.taskflows.container_shell.demo_network_analysis \
+# -g capture=sample.pcap
+
+seclab-taskflow-agent:
+ filetype: taskflow
+ version: "1.0"
+
+globals:
+ capture: sample.pcap
+
+taskflow:
+ - task:
+ must_complete: true
+ headless: true
+ agents:
+ - seclab_taskflow_agent.personalities.assistant
+ toolboxes:
+ - seclab_taskflows.toolboxes.container_shell_network_analysis
+ env:
+ CONTAINER_WORKSPACE: "{{ env('CONTAINER_WORKSPACE') }}"
+ user_prompt: |
+ Analyse the packet capture at /workspace/{{ globals.capture }}.
+
+ Work through each step below and record findings:
+
+ 1. Confirm the file and get a summary:
+ `tshark -r /workspace/{{ globals.capture }} -q -z io,phs`
+
+ 2. List all unique conversations (IP pairs):
+ `tshark -r /workspace/{{ globals.capture }} -q -z conv,ip`
+
+ 3. List all DNS queries:
+ `tshark -r /workspace/{{ globals.capture }} -Y dns -T fields -e dns.qry.name | sort -u`
+
+ 4. List all HTTP requests (method, host, URI):
+ `tshark -r /workspace/{{ globals.capture }} -Y http.request -T fields \
+ -e http.request.method -e http.host -e http.request.uri | sort -u`
+
+ 5. Show any cleartext credentials (basic auth, form data):
+ `tshark -r /workspace/{{ globals.capture }} -Y "http contains \"password\" or http contains \"passwd\"" \
+ -T fields -e http.file_data 2>/dev/null | head -20`
+
+ 6. Extract TLS SNI values (reveals contacted hostnames even in encrypted traffic):
+ `tshark -r /workspace/{{ globals.capture }} -Y tls.handshake.extensions_server_name \
+ -T fields -e tls.handshake.extensions_server_name | sort -u`
+
+ 7. Check for large outbound data transfers (potential exfiltration):
+ `tshark -r /workspace/{{ globals.capture }} -q -z conv,tcp | sort -k 3 -n -r | head -20`
+
+ Based on the above, report:
+ - What hosts and services were contacted?
+ - Were any credentials or sensitive data visible in cleartext?
+ - Are there signs of data exfiltration, C2 beaconing, or other suspicious patterns?
+ - Any notable DNS or TLS observations?
+
+ Write a concise analysis report.
diff --git a/src/seclab_taskflows/taskflows/container_shell/demo_sast.yaml b/src/seclab_taskflows/taskflows/container_shell/demo_sast.yaml
new file mode 100644
index 0000000..627a934
--- /dev/null
+++ b/src/seclab_taskflows/taskflows/container_shell/demo_sast.yaml
@@ -0,0 +1,54 @@
+# SPDX-FileCopyrightText: GitHub, Inc.
+# SPDX-License-Identifier: MIT
+
+# Demo: SAST container shell.
+# Analyses source code in CONTAINER_WORKSPACE using semgrep, ctags, and pyan3.
+# Example:
+# CONTAINER_WORKSPACE=/path/to/src python -m seclab_taskflow_agent \
+# -t seclab_taskflows.taskflows.container_shell.demo_sast \
+# -g target=mymodule.py
+
+seclab-taskflow-agent:
+ filetype: taskflow
+ version: "1.0"
+
+globals:
+ target: "."
+
+taskflow:
+ - task:
+ must_complete: true
+ headless: true
+ agents:
+ - seclab_taskflow_agent.personalities.assistant
+ toolboxes:
+ - seclab_taskflows.toolboxes.container_shell_sast
+ env:
+ CONTAINER_WORKSPACE: "{{ env('CONTAINER_WORKSPACE') }}"
+ user_prompt: |
+ Perform static analysis on the source code at /workspace/{{ globals.target }}.
+
+ 1. Directory overview:
+ `tree /workspace/{{ globals.target }} 2>/dev/null | head -40`
+
+ 2. Locate source files:
+ `fd -e py -e c -e h /workspace/{{ globals.target }} | head -30`
+
+ 3. Security scan with semgrep (report findings only, suppress progress):
+ `semgrep scan --config=auto --quiet /workspace/{{ globals.target }} 2>/dev/null`
+ If semgrep requires network access and it is unavailable, note that and skip.
+
+ 4. Symbol index with ctags:
+ `ctags -R --fields=+ne -f /tmp/tags /workspace/{{ globals.target }} && grep -v "^!" /tmp/tags | head -40`
+
+ 5. Call graph (Python only — skip if no .py files):
+ `pyan3 $(fd -e py /workspace/{{ globals.target }} | tr '\n' ' ') --dot --no-defines 2>/dev/null | head -60`
+
+ 6. Cross-reference entry points with GNU global:
+ `cd /workspace/{{ globals.target }} && gtags --compact -v 2>/dev/null && global -x main 2>/dev/null || echo "gtags: no entry point found"`
+
+ Based on the above, summarize:
+ - Security findings from semgrep (severity, rule, file, line)
+ - Key functions/classes and their call relationships
+ - Identified entry points and reachable sensitive operations
+ - Areas recommended for deeper review
diff --git a/src/seclab_taskflows/toolboxes/container_shell_base.yaml b/src/seclab_taskflows/toolboxes/container_shell_base.yaml
new file mode 100644
index 0000000..206acfd
--- /dev/null
+++ b/src/seclab_taskflows/toolboxes/container_shell_base.yaml
@@ -0,0 +1,38 @@
+# SPDX-FileCopyrightText: GitHub, Inc.
+# SPDX-License-Identifier: MIT
+
+seclab-taskflow-agent:
+ filetype: toolbox
+ version: "1.0"
+
+server_params:
+ kind: stdio
+ command: python
+ args: ["-m", "seclab_taskflows.mcp_servers.container_shell"]
+ env:
+ CONTAINER_IMAGE: "seclab-shell-base:latest"
+ CONTAINER_WORKSPACE: "{{ env('CONTAINER_WORKSPACE', required=False) }}"
+ CONTAINER_TIMEOUT: "{{ env('CONTAINER_TIMEOUT', '30') }}"
+ LOG_DIR: "{{ env('LOG_DIR') }}"
+
+confirm:
+ - shell_exec
+
+server_prompt: |
+ ## Container Shell (base)
+ You have access to an isolated Docker container. Use `shell_exec` to run commands.
+ The working directory is /workspace (mapped from the host workspace if configured).
+
+ Available tools in this container:
+ - bash, coreutils (ls, cat, grep, find, sed, awk, sort, uniq, wc, ...)
+ - file — identify file type by magic bytes
+ - strings — extract printable strings from binary files
+ - objdump — disassemble and dump object/binary files
+ - readelf — display ELF binary structure
+ - nm — list symbols from object files
+ - xxd / hexdump — hex inspection
+ - python3 — scripting
+ - curl / wget — HTTP requests
+ - git — version control
+
+ All commands run inside the container. Output includes stdout, stderr, and exit code.
diff --git a/src/seclab_taskflows/toolboxes/container_shell_malware_analysis.yaml b/src/seclab_taskflows/toolboxes/container_shell_malware_analysis.yaml
new file mode 100644
index 0000000..332eb87
--- /dev/null
+++ b/src/seclab_taskflows/toolboxes/container_shell_malware_analysis.yaml
@@ -0,0 +1,45 @@
+# SPDX-FileCopyrightText: GitHub, Inc.
+# SPDX-License-Identifier: MIT
+
+seclab-taskflow-agent:
+ filetype: toolbox
+ version: "1.0"
+
+server_params:
+ kind: stdio
+ command: python
+ args: ["-m", "seclab_taskflows.mcp_servers.container_shell"]
+ env:
+ CONTAINER_IMAGE: "seclab-shell-malware-analysis:latest"
+ CONTAINER_WORKSPACE: "{{ env('CONTAINER_WORKSPACE', required=False) }}"
+ CONTAINER_TIMEOUT: "{{ env('CONTAINER_TIMEOUT', '60') }}"
+ LOG_DIR: "{{ env('LOG_DIR') }}"
+
+confirm:
+ - shell_exec
+
+server_prompt: |
+ ## Container Shell (malware analysis)
+ You have access to an isolated Docker container for binary and malware analysis.
+ The working directory is /workspace — place samples here before analysis.
+ ALL tooling runs inside the container. Note: /workspace is bind-mounted from
+ the host, so files written there are visible on the host side as well.
+
+ Available tools:
+ - file, strings, xxd, hexdump — initial triage / string extraction
+ - objdump, readelf, nm — ELF structure and disassembly
+ - radare2 (r2, rabin2, rasm2) — reverse engineering and disassembly framework
+ - binwalk — firmware / archive extraction and analysis
+ - yara — pattern/signature matching
+ - exiftool — metadata extraction
+ - checksec — binary security feature detection
+ - capstone (Python) — disassembly library for scripting
+ - pwntools (Python) — exploit development / binary analysis library
+ - volatility3 — memory forensics framework
+ - python3 — scripting and automation
+
+ Recommended workflow:
+ 1. `file ` — identify type
+ 2. `strings -n 8 ` — extract strings
+ 3. `rabin2 -I ` — binary info (arch, bits, security features)
+ 4. `r2 -A -q -c "pdf @ main" ` — disassemble main function
diff --git a/src/seclab_taskflows/toolboxes/container_shell_network_analysis.yaml b/src/seclab_taskflows/toolboxes/container_shell_network_analysis.yaml
new file mode 100644
index 0000000..81923e4
--- /dev/null
+++ b/src/seclab_taskflows/toolboxes/container_shell_network_analysis.yaml
@@ -0,0 +1,34 @@
+# SPDX-FileCopyrightText: GitHub, Inc.
+# SPDX-License-Identifier: MIT
+
+seclab-taskflow-agent:
+ filetype: toolbox
+ version: "1.0"
+
+server_params:
+ kind: stdio
+ command: python
+ args: ["-m", "seclab_taskflows.mcp_servers.container_shell"]
+ env:
+ CONTAINER_IMAGE: "seclab-shell-network-analysis:latest"
+ CONTAINER_WORKSPACE: "{{ env('CONTAINER_WORKSPACE', required=False) }}"
+ CONTAINER_TIMEOUT: "{{ env('CONTAINER_TIMEOUT', '30') }}"
+ LOG_DIR: "{{ env('LOG_DIR') }}"
+
+confirm:
+ - shell_exec
+
+server_prompt: |
+ ## Container Shell (network analysis)
+ You have access to an isolated Docker container for network analysis and recon.
+ Files (e.g. pcap captures) are available at /workspace.
+
+ Available tools:
+ - nmap — network discovery and port scanning
+ - tcpdump — packet capture and inspection
+ - tshark — Wireshark CLI for pcap analysis
+ - netcat (nc) — TCP/UDP connections and simple servers
+ - dig / nslookup — DNS lookups
+ - curl / wget — HTTP inspection
+ - jq — JSON parsing and transformation
+ - httpie (http) — human-friendly HTTP client
diff --git a/src/seclab_taskflows/toolboxes/container_shell_sast.yaml b/src/seclab_taskflows/toolboxes/container_shell_sast.yaml
new file mode 100644
index 0000000..9a2ec79
--- /dev/null
+++ b/src/seclab_taskflows/toolboxes/container_shell_sast.yaml
@@ -0,0 +1,45 @@
+# SPDX-FileCopyrightText: GitHub, Inc.
+# SPDX-License-Identifier: MIT
+
+seclab-taskflow-agent:
+ filetype: toolbox
+ version: "1.0"
+
+server_params:
+ kind: stdio
+ command: python
+ args: ["-m", "seclab_taskflows.mcp_servers.container_shell"]
+ env:
+ CONTAINER_IMAGE: "seclab-shell-sast:latest"
+ CONTAINER_WORKSPACE: "{{ env('CONTAINER_WORKSPACE', required=False) }}"
+ CONTAINER_TIMEOUT: "{{ env('CONTAINER_TIMEOUT', '60') }}"
+ LOG_DIR: "{{ env('LOG_DIR') }}"
+
+confirm:
+ - shell_exec
+
+server_prompt: |
+ ## Container Shell (SAST)
+ You have access to an isolated Docker container for static analysis and code exploration.
+ Source code is available at /workspace. All tools run inside the container.
+
+ Available tools:
+ - semgrep — SAST scanner; run with `semgrep scan --config=auto /workspace`
+ - pyan3 — Python static call graph generator (dot output for graphviz)
+ - ctags — Multi-language tag/symbol index (`ctags -R .`)
+ - global (gtags/global) — Source code cross-reference: `gtags` to index, `global -r ` to find callers
+ - cscope — C/C++ cross-reference browser (`cscope -R -b` to build, `cscope -R -L -2 ` for callers)
+ - graphviz (dot) — Render dot graphs: `dot -Tpng callgraph.dot -o callgraph.png`
+ - ripgrep (rg) — Fast regex search: `rg /workspace`
+ - fd — Fast file finder: `fd -e py /workspace`
+ - tree — Directory structure: `tree /workspace`
+ - grep, sed, awk, find — Standard code exploration
+
+ Typical call graph workflow (Python):
+ 1. `fd -e py /workspace | head -30` — find Python files
+ 2. `pyan3 $(fd -e py /workspace | tr '\n' ' ') --dot --no-defines > /tmp/cg.dot`
+ 3. `dot -Tsvg /tmp/cg.dot -o /tmp/cg.svg && cat /tmp/cg.svg | head -100`
+
+ Typical call graph workflow (C):
+ 1. `cd /workspace && ctags -R --fields=+ne .`
+ 2. `cd /workspace && cscope -R -b && cscope -R -L -2 `
diff --git a/tests/test_container_shell.py b/tests/test_container_shell.py
new file mode 100644
index 0000000..8be2716
--- /dev/null
+++ b/tests/test_container_shell.py
@@ -0,0 +1,218 @@
+# SPDX-FileCopyrightText: GitHub, Inc.
+# SPDX-License-Identifier: MIT
+
+import subprocess
+from unittest.mock import MagicMock, patch
+
+import pytest
+
+import seclab_taskflows.mcp_servers.container_shell as cs_mod
+from seclab_taskflow_agent.available_tools import AvailableTools
+
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+def _make_proc(returncode=0, stdout="", stderr=""):
+ proc = MagicMock()
+ proc.returncode = returncode
+ proc.stdout = stdout
+ proc.stderr = stderr
+ return proc
+
+
+def _reset_container():
+ """Reset global container state between tests."""
+ cs_mod._container_name = None
+
+
+# ---------------------------------------------------------------------------
+# _start_container tests
+# ---------------------------------------------------------------------------
+
+class TestStartContainer:
+ def setup_method(self):
+ _reset_container()
+
+ def test_start_container_success(self):
+ with (
+ patch.object(cs_mod, "CONTAINER_IMAGE", "test-image:latest"),
+ patch.object(cs_mod, "CONTAINER_WORKSPACE", "/host/workspace"),
+ patch("subprocess.run", return_value=_make_proc(returncode=0)) as mock_run,
+ ):
+ name = cs_mod._start_container()
+ assert name.startswith("seclab-shell-")
+ cmd = mock_run.call_args[0][0]
+ assert "docker" in cmd
+ assert "run" in cmd
+ assert "--name" in cmd
+ assert "-v" in cmd
+ assert "/host/workspace:/workspace" in cmd
+ assert "test-image:latest" in cmd
+ assert "tail" in cmd
+
+ def test_start_container_no_workspace(self):
+ with (
+ patch.object(cs_mod, "CONTAINER_IMAGE", "test-image:latest"),
+ patch.object(cs_mod, "CONTAINER_WORKSPACE", ""),
+ patch("subprocess.run", return_value=_make_proc(returncode=0)) as mock_run,
+ ):
+ name = cs_mod._start_container()
+ assert name.startswith("seclab-shell-")
+ cmd = mock_run.call_args[0][0]
+ assert "-v" not in cmd
+
+ def test_start_container_failure(self):
+ with (
+ patch.object(cs_mod, "CONTAINER_IMAGE", "missing-image:latest"),
+ patch.object(cs_mod, "CONTAINER_WORKSPACE", ""),
+ patch("subprocess.run", return_value=_make_proc(returncode=1, stderr="image not found")),
+ ):
+ with pytest.raises(RuntimeError, match="docker run failed"):
+ cs_mod._start_container()
+
+ def test_start_container_rejects_colon_in_workspace(self):
+ with (
+ patch.object(cs_mod, "CONTAINER_IMAGE", "test-image:latest"),
+ patch.object(cs_mod, "CONTAINER_WORKSPACE", "/host/path:ro"),
+ ):
+ with pytest.raises(RuntimeError, match="CONTAINER_WORKSPACE must not contain a colon"):
+ cs_mod._start_container()
+
+ def test_start_container_rejects_empty_image(self):
+ with (
+ patch.object(cs_mod, "CONTAINER_IMAGE", ""),
+ patch.object(cs_mod, "CONTAINER_WORKSPACE", ""),
+ ):
+ with pytest.raises(RuntimeError, match="CONTAINER_IMAGE is not set"):
+ cs_mod._start_container()
+
+
+# ---------------------------------------------------------------------------
+# shell_exec tests
+# ---------------------------------------------------------------------------
+
+class TestShellExec:
+ def setup_method(self):
+ _reset_container()
+
+ def test_shell_exec_lazy_start(self):
+ start_proc = _make_proc(returncode=0)
+ exec_proc = _make_proc(returncode=0, stdout="hello\n")
+ with (
+ patch.object(cs_mod, "CONTAINER_IMAGE", "test-image:latest"),
+ patch.object(cs_mod, "CONTAINER_WORKSPACE", ""),
+ patch("subprocess.run", side_effect=[start_proc, exec_proc]),
+ ):
+ assert cs_mod._container_name is None
+ result = cs_mod.shell_exec.fn(command="echo hello")
+ assert cs_mod._container_name is not None
+ assert "hello" in result
+
+ def test_shell_exec_runs_command(self):
+ cs_mod._container_name = "seclab-shell-testtest"
+ exec_proc = _make_proc(returncode=0, stdout="output\n")
+ with patch("subprocess.run", return_value=exec_proc) as mock_run:
+ result = cs_mod.shell_exec.fn(command="echo output", workdir="/workspace")
+ cmd = mock_run.call_args[0][0]
+ assert "docker" in cmd
+ assert "exec" in cmd
+ assert "-w" in cmd
+ assert "/workspace" in cmd
+ assert "seclab-shell-testtest" in cmd
+ assert "echo output" in cmd
+ assert "output" in result
+
+ def test_shell_exec_includes_exit_code(self):
+ cs_mod._container_name = "seclab-shell-testtest"
+ exec_proc = _make_proc(returncode=0, stdout="done\n")
+ with patch("subprocess.run", return_value=exec_proc):
+ result = cs_mod.shell_exec.fn(command="true")
+ assert "[exit code: 0]" in result
+
+ def test_shell_exec_nonzero_exit(self):
+ cs_mod._container_name = "seclab-shell-testtest"
+ exec_proc = _make_proc(returncode=1, stdout="", stderr="error\n")
+ with patch("subprocess.run", return_value=exec_proc):
+ result = cs_mod.shell_exec.fn(command="false")
+ assert "[exit code: 1]" in result
+ assert "error" in result
+
+ def test_shell_exec_timeout(self):
+ cs_mod._container_name = "seclab-shell-testtest"
+ with patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd="docker", timeout=5)):
+ result = cs_mod.shell_exec.fn(command="sleep 999", timeout=5)
+ assert "timeout" in result
+
+ def test_shell_exec_start_failure_returns_error(self):
+ _reset_container()
+ with (
+ patch.object(cs_mod, "CONTAINER_IMAGE", "bad-image:latest"),
+ patch.object(cs_mod, "CONTAINER_WORKSPACE", ""),
+ patch("subprocess.run", return_value=_make_proc(returncode=1, stderr="image not found")),
+ ):
+ result = cs_mod.shell_exec.fn(command="echo hi")
+ assert "Failed to start container" in result
+ assert cs_mod._container_name is None
+
+
+# ---------------------------------------------------------------------------
+# _stop_container tests
+# ---------------------------------------------------------------------------
+
+class TestStopContainer:
+ def setup_method(self):
+ _reset_container()
+
+ def test_stop_container_called_on_atexit(self):
+ cs_mod._container_name = "seclab-shell-tostop"
+ with patch("subprocess.run", return_value=_make_proc(returncode=0)) as mock_run:
+ cs_mod._stop_container()
+ cmd = mock_run.call_args[0][0]
+ assert "docker" in cmd
+ assert "stop" in cmd
+ assert "seclab-shell-tostop" in cmd
+ assert cs_mod._container_name is None
+
+ def test_stop_container_no_op_when_none(self):
+ cs_mod._container_name = None
+ with patch("subprocess.run") as mock_run:
+ cs_mod._stop_container()
+ mock_run.assert_not_called()
+
+ def test_stop_container_clears_name_on_failure(self):
+ cs_mod._container_name = "seclab-shell-tostop"
+ with patch("subprocess.run", return_value=_make_proc(returncode=1, stderr="not found")):
+ cs_mod._stop_container()
+ assert cs_mod._container_name is None
+
+
+# ---------------------------------------------------------------------------
+# Toolbox YAML validation
+# ---------------------------------------------------------------------------
+
+class TestToolboxYaml:
+ def test_toolbox_yaml_valid_base(self):
+ tools = AvailableTools()
+ result = tools.get_toolbox("seclab_taskflows.toolboxes.container_shell_base")
+ assert result is not None
+ assert result["seclab-taskflow-agent"]["filetype"] == "toolbox"
+
+ def test_toolbox_yaml_valid_malware(self):
+ tools = AvailableTools()
+ result = tools.get_toolbox("seclab_taskflows.toolboxes.container_shell_malware_analysis")
+ assert result is not None
+ assert result["seclab-taskflow-agent"]["filetype"] == "toolbox"
+
+ def test_toolbox_yaml_valid_network(self):
+ tools = AvailableTools()
+ result = tools.get_toolbox("seclab_taskflows.toolboxes.container_shell_network_analysis")
+ assert result is not None
+ assert result["seclab-taskflow-agent"]["filetype"] == "toolbox"
+
+ def test_toolbox_yaml_valid_sast(self):
+ tools = AvailableTools()
+ result = tools.get_toolbox("seclab_taskflows.toolboxes.container_shell_sast")
+ assert result is not None
+ assert result["seclab-taskflow-agent"]["filetype"] == "toolbox"