Skip to content

Commit b9dc1f0

Browse files
committed
feat Adding Sysdig CLI scanner tool for local servers
Signed-off-by: S3B4SZ17 <[email protected]>
1 parent ae08b12 commit b9dc1f0

File tree

11 files changed

+312
-129
lines changed

11 files changed

+312
-129
lines changed

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,6 @@ COPY --from=builder --chown=app:app /app/app_config.yaml /app
3333

3434
RUN pip install /app/sysdig_mcp_server.tar.gz
3535

36+
USER 1001:1001
37+
3638
ENTRYPOINT ["sysdig-mcp-server"]

app_config.yaml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ mcp:
1111
transport: stdio
1212
host: "localhost"
1313
port: 8080
14+
allowed_tools:
15+
- "events-feed"
16+
- "sysdig-cli-scanner" # Only available in stdio local transport mode
17+
- "vulnerability-management"
18+
- "inventory"
19+
- "sysdig-sage"

tools/cli_scanner/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+

tools/cli_scanner/tool.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
"""
2+
CLI Scanner Tool for Sysdig
3+
4+
This tool helps you use the Sysdig CLI Scanner to analyze your development files and directories.
5+
"""
6+
7+
import logging
8+
import os
9+
import subprocess
10+
from typing import Literal, Optional
11+
12+
from utils.app_config import get_app_config
13+
14+
logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=os.environ.get("LOGLEVEL", "ERROR"))
15+
16+
log = logging.getLogger(__name__)
17+
18+
# Load app config (expects keys: mcp.host, mcp.port, mcp.transport)
19+
app_config = get_app_config()
20+
21+
22+
class CLIScannerTool:
23+
"""
24+
A class to encapsulate the tools for interacting with the Sysdig CLI Scanner.
25+
"""
26+
27+
cmd: str = "sysdig-cli-scanner"
28+
default_args: list = [
29+
"--loglevel=err",
30+
"--apiurl=" + app_config["sysdig"]["host"],
31+
]
32+
iac_default_args: list = [
33+
"--iac",
34+
"--group-by=violation",
35+
"--recursive",
36+
]
37+
38+
exit_code_explained: str = """
39+
Exit codes:
40+
0: Scan evaluation "pass"
41+
1: Scan evaluation "fail"
42+
2: Invalid parameters
43+
3: Internal error
44+
"""
45+
46+
def check_sysdig_cli_installed(self) -> None:
47+
"""
48+
Checks if the Sysdig CLI Scanner is installed by verifying the existence of the 'sysdig-cli-scanner' command.
49+
"""
50+
try:
51+
# Attempt to run 'sysdig-cli-scanner --version' to check if it's installed
52+
result = subprocess.run([self.cmd, "--version"], capture_output=True, text=True, check=True)
53+
log.info(f"Sysdig CLI Scanner is installed: {result.stdout.strip()}")
54+
except subprocess.CalledProcessError as e:
55+
error: dict = {
56+
"error": "Sysdig CLI Scanner is not installed. Check the docs to install it here: https://docs.sysdig.com/en/sysdig-secure/install-vulnerability-cli-scanner/#deployment"
57+
}
58+
e.output = error
59+
raise e
60+
61+
def check_env_credentials(self) -> None:
62+
"""
63+
Checks if the necessary environment variables for Sysdig Secure are set.
64+
Raises:
65+
EnvironmentError: If the SYSDIG_SECURE_TOKEN or SYSDIG_HOST environment variables are not set.
66+
"""
67+
sysdig_secure_token = os.environ.get("SYSDIG_SECURE_TOKEN")
68+
sysdig_host = os.environ.get("SYSDIG_HOST", app_config["sysdig"]["host"])
69+
if not sysdig_secure_token:
70+
log.error("SYSDIG_SECURE_TOKEN environment variable is not set.")
71+
raise EnvironmentError("SYSDIG_SECURE_TOKEN environment variable is not set.")
72+
else:
73+
os.environ["SECURE_API_TOKEN"] = sysdig_secure_token # Ensure the token is set in the environment
74+
if not sysdig_host:
75+
log.error("SYSDIG_HOST environment variable is not set.")
76+
raise EnvironmentError("SYSDIG_HOST environment variable is not set.")
77+
78+
def run_sysdig_cli_scanner(
79+
self,
80+
image: Optional[str] = None,
81+
directory_path: Optional[str] = None,
82+
mode: Literal["vulnerability", "iac"] = "vulnerability",
83+
) -> dict:
84+
"""
85+
Analyzes a Container image for vulnerabilities using the Sysdig CLI Scanner.
86+
Args:
87+
image (str): The name of the container image to analyze.
88+
directory_path (str): The path to the directory containing IaC files to analyze.
89+
mode ["vulnerability", "iac"]: The mode of analysis, either "vulnerability" or "iac".
90+
Defaults to "vulnerability".
91+
Returns:
92+
dict: A dictionary containing the output of the analysis of vulnerabilities.
93+
Raises:
94+
Exception: If the Sysdig CLI Scanner encounters an error.
95+
"""
96+
# Check if Sysdig CLI Scanner is installed and environment credentials are set
97+
self.check_sysdig_cli_installed()
98+
self.check_env_credentials()
99+
100+
# Prepare the command based on the mode
101+
if mode == "iac":
102+
log.info("Running Sysdig CLI Scanner in IaC mode.")
103+
cmd = [self.cmd] + self.default_args + self.iac_default_args + [directory_path]
104+
else:
105+
log.info("Running Sysdig CLI Scanner in vulnerability mode.")
106+
# Default to vulnerability mode
107+
cmd = [self.cmd] + self.default_args + [image]
108+
109+
try:
110+
# Run the command
111+
with open("sysdig_cli_scanner_output.json", "w") as output_file:
112+
result = subprocess.run(cmd, text=True, check=True, stdout=output_file, stderr=subprocess.PIPE)
113+
output_result = output_file.read()
114+
output_file.close()
115+
return {
116+
"exit_code": result.returncode,
117+
"output": output_result,
118+
"exit_codes_explained": self.exit_code_explained,
119+
}
120+
# Handle non-zero exit codes speically exit code 1
121+
except subprocess.CalledProcessError as e:
122+
log.warning(f"Sysdig CLI Scanner returned non-zero exit code: {e.returncode}")
123+
if e.returncode in [2, 3]:
124+
log.error(f"Sysdig CLI Scanner encountered an error: {e.stderr.strip()}")
125+
result: dict = {
126+
"error": "Error running Sysdig CLI Scanner",
127+
"exit_code": e.returncode,
128+
"output": e.stderr.strip(),
129+
"exit_codes_explained": self.exit_code_explained,
130+
}
131+
raise Exception(result)
132+
else:
133+
with open("sysdig_cli_scanner_output.json", "r") as output_file:
134+
output_result = output_file.read()
135+
result: dict = {
136+
"exit_code": e.returncode,
137+
"stdout": e.stdout,
138+
"output": output_result,
139+
"exit_codes_explained": self.exit_code_explained,
140+
}
141+
os.remove("sysdig_cli_scanner_output.json")
142+
return result
143+
# Handle any other exceptions that may occur and exit codes 2 and 3
144+
except Exception as e:
145+
raise e

tools/inventory/tool.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,8 @@
1818
from utils.query_helpers import create_standard_response
1919

2020
# Configure logging
21-
log = logging.getLogger(__name__)
2221
logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=os.environ.get("LOGLEVEL", "ERROR"))
22+
log = logging.getLogger(__name__)
2323

2424
# Load app config (expects keys: mcp.host, mcp.port, mcp.transport)
2525
app_config = get_app_config()
@@ -113,7 +113,7 @@ def tool_list_resources(
113113

114114
return response
115115
except ApiException as e:
116-
logging.error("Exception when calling InventoryApi->get_resources: %s\n" % e)
116+
log.error(f"Exception when calling InventoryApi->get_resources: {e}")
117117
raise e
118118

119119
def tool_get_resource(

tools/sysdig_sage/tool.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@
1717
from utils.sysdig.api import initialize_api_client
1818
from utils.query_helpers import create_standard_response
1919

20-
log = logging.getLogger(__name__)
2120
logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=os.environ.get("LOGLEVEL", "ERROR"))
21+
log = logging.getLogger(__name__)
2222

2323
app_config = get_app_config()
2424

utils/app_config.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@
99
from typing import Optional
1010

1111
# Set up logging
12-
log = logging.getLogger(__name__)
1312
logging.basicConfig(format="%(asctime)s-%(process)d-%(levelname)s- %(message)s", level=os.environ.get("LOGLEVEL", "ERROR"))
13+
log = logging.getLogger(__name__)
1414

1515
# app_config singleton
1616
_app_config: Optional[dict] = None
@@ -61,9 +61,11 @@ def load_app_config() -> dict:
6161
def get_app_config() -> dict:
6262
"""
6363
Get the the overall app config
64+
This function uses a singleton pattern to ensure the config is loaded only once.
65+
If the config is already loaded, it returns the existing config.
6466
6567
Returns:
66-
dict: The app config loaded from the YAML file, or an empty dict if the file
68+
dict: The app config loaded from the YAML file, or an empty dict if the file does not exist or is invalid.
6769
"""
6870
global _app_config
6971
if _app_config is None:

0 commit comments

Comments
 (0)