Is affaan-m/ECC safe?
- Python shell/command execution
- Node.js shell/command execution
- Defense-evasion command idiom
ecc-universal is an AI mcp_server analyzed by SkillTotal's deterministic static scanner. The scan found no malicious indicators, though 14 risky constructs are reported for review. It can: dynamic code execution, filesystem read, filesystem write, mcp tools detected, network egress and shell execution — capabilities are what the code can do, not a verdict on intent. Risk score 30/100 (medium).
ecc-universal 2.2.2
Automated static-analysis result. It can contain false positives and false negatives, and is not a claim about the intent of affaan-m/ECC's authors. Report a false positive.
Behavioral traits
How this component maps to the CSA agentic threat model. Descriptive — it never affects the risk score.
Findings (14)
The code turns strings into live code at runtime (eval / new Function / exec).
{ pattern: /\beval\s*\(/g, name: "eval() usage - potential code injection" },Why it matters: If those strings aren't fixed and trusted, they become a way to run arbitrary code.
Fix: Avoid evaluating dynamically constructed code; if unavoidable, ensure the input is a trusted constant and never derived from external data.
An MCP server entry launches a command on your host.
"command": "npx",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
"command": "nexus",
Why it matters: Trusting the manifest means running that binary — verify what it is and where it comes from.
Fix: Verify the launched command and its source before trusting this MCP server configuration.
A hardcoded credential (API key, token, or private key) is shipped in the code.
"check": "'use strict';\n// Hidden grader for sentinel-api: runs exploit probes and functional regression\n// probes against the agent's service, in-process, plus static source checks.\n// Prints ECC_EVAL_SCORE and always exits 0.\nconst fs …
const GRADER_TOKEN = 'ecc-…[redacted, 27 chars]';
Why it matters: Anyone who gets the package gets the secret — rotate it and load secrets at runtime instead.
Fix: Remove the secret from the code, rotate it immediately, and load credentials from the environment or a secrets manager at runtime.
A command uses a known defense-evasion idiom: PowerShell execution-policy bypass / encoded command / hidden window, macOS code-signing bypass, or launching a payload from a world-writable temp directory. These are hallmarks of droppers and rarely appear in legitimate code. (19 occurrence(s) shown as evidence).
return ['powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', WINDOWS_BROWSER_COMMAND]];
Fix: Verify why the component bypasses execution policy / code signing or runs from a temp directory; these patterns are characteristic of malware staging.
The component can run operating-system commands or spawn processes.
const { execFileSync } = require('child_process');import { execFileSync } from "child_process"import { execFile } from "node:child_process"const { spawnSync } = require('node:child_process');const result = spawnSync('security', ['find-generic-password', '-s', 'Claude Code-credentials', '-w'],const resolved = spawnSync('git', ['-C', repoRoot, 'rev-parse', '--verify', `${pin.sha}^{commit}`],const archive = spawnSync('git', ['-C', repoRoot, 'archive', '--format=tar', '-o', tar, pin.sha, 'skills'],? spawnSync('tar', ['-xf', tar, '-C', destination], { encoding: 'utf8', shell: false, timeout: 60000, killSignal: 'SIGKILL' })const result = spawnSync(process.execPath, checkArguments(fs.realpathSync(cwd), name, step !== null), { cwd, encoding: 'utf8',const { spawn, spawnSync } = require('node:child_process');const result = spawnSync(command, args, { ...options, encoding: 'utf8', timeout: 60000,const server = spawn(process.env.ECC_NATIVE_CODEX || 'codex', ['app-server', '--stdio'], {const { spawnSync } = require('node:child_process');const result = spawnSync(process.execPath, [path.join(repoRoot, 'scripts/ecc.js'), 'profile', ...args, '--json'], {const cli = spawnSync(process.execPath, [path.join(repoRoot, 'scripts/ecc.js'),
const native = spawnSync(process.execPath, [path.join(__dirname, script)], {const { spawnSync } = require('node:child_process');const result = spawnSync(command, args, { cwd: repoRoot, encoding: 'utf8',spawnSync('podman', ['image', 'rm', image], { stdio: 'ignore', timeout: 60000 });const { spawn } = require('node:child_process');const child = spawn(executable, args, { cwd, env: process.env, stdio: ['ignore', 'pipe', 'pipe'], shell: false });const { spawnSync } = require('node:child_process');const child = spawnSync(process.execPath, [cli, 'profile', ...args, '--json'], {const { spawnSync } = require('child_process');const result = spawnSync(executable, argv, {Why it matters: Powerful and often legitimate — confirm the commands aren't built from untrusted input.
Fix: Confirm the command and its arguments are fully controlled and not derived from untrusted input; prefer execFile with an argument array.
The component can run operating-system commands or spawn processes.
subprocess.Popen(argv, **kwargs) # noqa: S603 - list argv, no shell=True, path validated above
result = subprocess.run(args, capture_output=True, text=True, timeout=5)
result = subprocess.run(
["git", "-C", project_root, "worktree", "list", "--porcelain"],
capture_output=True, text=True, timeout=5
)result = subprocess.run(
["git", "-C", project_root, "remote", "get-url", "origin"],
capture_output=True, text=True, timeout=5
)result = subprocess.run(
["claude", "-p", prompt, "--model", model, "--output-format", "text"],
capture_output=True,
text=True,
timeout=60,
)result = subprocess.run(
[
"claude", "-p", scenario.prompt,
"--model", model,
"--max-turns", str(max_turns),
"--add-dir", str(sandbox_dir),
"--allowedTools", "Read,Write,Ed …subprocess.run(["git", "init"], cwd=sandbox_dir, capture_output=True)
subprocess.run(parts, cwd=sandbox_dir, capture_output=True)
result = subprocess.run(
["claude", "-p", prompt, "--model", model, "--output-format", "text"],
capture_output=True,
text=True,
timeout=120,
)result = subprocess.run(
["claude", "-p", prompt, "--model", model, "--output-format", "text"],
capture_output=True,
text=True,
timeout=120,
)proc = subprocess.run(_command(cmd))
rc = subprocess.run(_command(vcmd)).returncode
proc = subprocess.run(cmd, capture_output=True, text=True)
proc = subprocess.run(cmd, capture_output=True)
proc = subprocess.run(cmd, capture_output=True, text=True)
proc = subprocess.run(cmd, capture_output=True, text=True)
proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
proc = subprocess.run(
["blender", "-b", "--python", script, "--", cfg],
capture_output=True, text=True, timeout=timeout,
)proc = subprocess.run([
"ffmpeg", "-nostdin", "-loglevel", "error", "-y",
"-framerate", f"{fps:g}", "-i", pattern,
"-c:v", "libx264", "-crf", "14", "-pix_fmt", "yuv420p", str(dst),
], capture_output=True, text=Tr …result = subprocess.run(
[
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"csv=p=0",
str(path),
],dec = subprocess.Popen(
[
"ffmpeg",
"-nostdin",
"-v",
"error",
"-ss",
str(start),
"-t",
str(duration …enc = subprocess.Popen(
[
"ffmpeg",
"-nostdin",
"-y",
"-v",
"error",
"-f",
"rawvideo …result = subprocess.run(
[
"ffprobe",
"-v",
"error",
"-select_streams",
"v:0",
"-count_frames",
"-show_entries",
"stream=avg_frame_rate, …result = subprocess.run(
[ffmpeg, "-v", "error", "-i", str(path), "-vf", filters,
"-an", "-vsync", "0", "-f", "null", "-"],
check=True,
capture_output=True,
text=True,
)result = subprocess.run(command, check=True, capture_output=True, text=True)
Why it matters: Powerful and often legitimate — confirm the commands aren't built from untrusted input.
Fix: Confirm the command and its arguments are fully controlled and not derived from untrusted input; avoid shell=True.
The component ships agent or IDE configuration that executes a command without a separate step: a Claude Code / Gemini CLI / Cursor hook, or a VS Code task that runs when the folder opens. Opening the project in that tool runs it with the developer's privileges. (16 occurrence(s) shown as evidence).
"command": "node .cursor/hooks/session-start.js",
"command": "node .cursor/hooks/session-end.js",
"command": "node .cursor/hooks/before-shell-execution-block-no-verify.js",
"command": "node .cursor/hooks/before-shell-execution.js",
"command": "node .cursor/hooks/after-shell-execution.js",
"command": "node .cursor/hooks/after-file-edit.js",
"command": "node .cursor/hooks/before-mcp-execution.js",
"command": "node .cursor/hooks/after-mcp-execution.js",
"command": "node .cursor/hooks/before-read-file.js",
"command": "node .cursor/hooks/before-submit-prompt.js",
"command": "node .cursor/hooks/subagent-start.js",
"command": "node .cursor/hooks/subagent-stop.js",
"command": "node .cursor/hooks/before-tab-file-read.js",
"command": "node .cursor/hooks/after-tab-file-edit.js",
"command": "node .cursor/hooks/pre-compact.js",
"command": "node .cursor/hooks/stop.js",
Fix: Read the command and anything it runs before opening this project in an agent or editor. A published package has no reason to ship project auto-run config.
The component reads files from disk.
const content = fs.readFileSync(filePath, 'utf8');
const content = fs.readFileSync(manifestPath, 'utf8');
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
const content = JSON.parse(fs.readFileSync(fullPath, "utf-8"))
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
const content = fs.readFileSync(pyprojectPath, "utf-8")
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8"))
const content = fs.readFileSync(filePath, "utf-8")
text = fs.readFileSync(path.join(ECC_ROOT, "rules", "common", file), "utf8").trim()
const parsed = JSON.parse(fs.readFileSync(file, "utf8")) as { packages?: unknown }const manifest = JSON.parse(fs.readFileSync(path.join(ECC_ROOT, "package.json"), "utf8")) as {function loadCorpus(file = CORPUS_PATH) { return JSON.parse(fs.readFileSync(file, 'utf8')); }const original = fs.readFileSync(source);
const after = fs.readFileSync(leased);
const prepared = fs.readFileSync(config);
pin = JSON.parse(fs.readFileSync(LEGACY_PIN_PATH, 'utf8')) } = {}) {const registration = JSON.parse(fs.readFileSync(flags.get('--registration'), 'utf8'));else if (entry.isFile()) files[relative] = fs.readFileSync(path.join(directory, entry.name), 'utf8');
const meta = JSON.parse(fs.readFileSync(path.join(directory, 'meta.json'), 'utf8'));
query: fs.readFileSync(path.join(stepsDir, name, 'query.md'), 'utf8').trim(),
check: fs.readFileSync(path.join(stepsDir, name, 'check.cjs'), 'utf8'),
const query = fs.readFileSync(path.join(directory, 'query.md'), 'utf8').trim();
query, files, check: fs.readFileSync(path.join(directory, 'check.cjs'), 'utf8') };
try { incident = fs.readFileSync(path.join(process.cwd(), 'INCIDENT.md'), 'utf8'); } catch { /* missing */ }.map(file => fs.readFileSync(file, 'utf8')).join('\n');Why it matters: Usually legitimate, but worth confirming it can't be steered into reading sensitive files.
Fix: Confirm which files are read and that paths cannot be influenced by untrusted input to reach sensitive locations.
The component writes or deletes files on disk.
fs.writeFileSync(manifestPath, content, 'utf8');
fs.rmSync(codebuddyFullPath, { recursive: true, force: true });fs.unlinkSync(fullPath);
fs.writeFileSync(leased, original, { flag: 'wx', mode: 0o600 });fs.writeFileSync(temp, after, { flag: 'wx', mode: 0o600 });} finally { fs.rmSync(temp, { force: true }); }fs.rmSync(leased, { force: true });fs.writeFileSync(config, prepared);
for (const entry of listing(plugins)) if (!preparedPlugins.has(entry)) fs.rmSync(path.join(plugins, entry), { recursive: true, force: true });if (!preparedCache.has(entry)) fs.rmSync(path.join(plugins, 'cache', entry), { recursive: true, force: true });fs.rmSync(tar, { force: true });fs.writeFileSync(file, source, { flag: 'wx' });fs.rmSync(file, { force: true });fs.writeFileSync(path.join(cwd, relative), content, { flag: 'wx' });writeIndex() { fs.writeFileSync(path.join(artifactDir, 'artifact-index.json'), `${JSON.stringify(index, null, 1)}\n`); },fs.rmSync(cwd, { recursive: true, force: true });} finally { if (harvester) harvester.writeIndex(); fs.rmSync(temp, { recursive: true, force: true }); }fs.writeFileSync(OUT, `${JSON.stringify(corpus, null, 1)}\n`);fs.rmSync(naiveDir, { recursive: true, force: true });fs.rmSync(refDir, { recursive: true, force: true });fs.rmSync(DATA_FILE, { force: true });fs.writeFileSync(DATA_FILE, 'garbage{{{');fs.rmSync(DATA_FILE, { force: true });fs.rmSync(DATA_FILE, { force: true });fs.writeFileSync(DATA_FILE, 'garbage{{{');Why it matters: Usually legitimate, but worth confirming the paths can't be controlled by untrusted input.
Fix: Confirm which files are written/deleted and that paths cannot be influenced by untrusted input.
The component reads files from disk.
with open(agent_path, 'r', encoding='utf-8') as f:
with open(skill_file, 'r', encoding='utf-8') as f:
with open(os.path.join(commands_dir, item), 'r', encoding='utf-8') as f:
with open(path) as f:
with open(path) as f:
with open(REGISTRY_FILE, encoding="utf-8") as f:
with open(REGISTRY_FILE, encoding="utf-8") as f:
content = file.read_text(encoding="utf-8")
with open(observations_file, encoding="utf-8") as f:
instincts = parse_instinct_file(file_path.read_text(encoding="utf-8"))
lines = from_file.read_text(encoding="utf-8").splitlines()
with open(obs_file, encoding="utf-8") as f:
content = path.read_text(encoding="utf-8")
content = source_file.read_text(encoding="utf-8")
with open(obs_file, encoding="utf-8") as f:
content = file_path.read_text(encoding="utf-8")
with open(tp, 'r', encoding='utf-8') as f:
prompt_template = (PROMPTS_DIR / "classifier.md").read_text()
text = path.read_text().strip()
raw = yaml.safe_load(path.read_text())
skill_content = skill_path.read_text()
prompt_template = (PROMPTS_DIR / "scenario_generator.md").read_text()
skill_content = skill_path.read_text()
prompt_template = (PROMPTS_DIR / "spec_generator.md").read_text()
zones = json.loads(grade.read_text()).get("zones") or []Why it matters: Usually legitimate, but worth confirming it can't be steered into reading sensitive files.
Fix: Confirm which files are read and that paths cannot be influenced by untrusted input to reach sensitive locations.
The component writes or deletes files on disk.
with open(AUDIT_FILE, "a", encoding="utf-8") as f:
lock_fd = open(lock_path, "w")
with open(tmp_file, "w", encoding="utf-8") as f:
with open(tmp_file, "w", encoding="utf-8") as f:
shutil.rmtree(project_dir)
shutil.copy2(file_path, target_path)
with open(into_file, "a", encoding="utf-8") as f:
output_file.write_text(output_content, encoding="utf-8")
out_path.write_text(output, encoding="utf-8")
output_file.write_text(output_content, encoding="utf-8")
output_file.write_text(output_content, encoding="utf-8")
(skill_dir / "SKILL.md").write_text(content, encoding="utf-8")
cmd_file.write_text(content, encoding="utf-8")
agent_file.write_text(content, encoding="utf-8")
with open(image_path, "wb") as f:
with open(output_path, "w") as f:
output_path.write_text(report)
shutil.rmtree(sandbox_dir)
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
dest.write_bytes(b"taste-forge dry-run placeholder\n")
manifest.write_text(json.dumps(record, indent=2), encoding="utf-8")
shutil.copy2(sp.lut_path, dest)
listing.write_text("".join(f"file '{c.resolve().as_posix()}'\n" for c in clips))path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
path.write_text(json.dumps(cadence.to_dict(), indent=2), encoding="utf-8")
Why it matters: Usually legitimate, but worth confirming the paths can't be controlled by untrusted input.
Fix: Confirm which files are written/deleted and that paths cannot be influenced by untrusted input.
The component makes outbound network requests.
fetch(`http://127.0.0.1:${port}/stats?type=${q.type}&from=${q.from}&to=${q.to}`).then(r => r.json())));const response = await fetch(`http://127.0.0.1:${port}/pastes`, {const denied = await fetch(`http://127.0.0.1:${port}/admin/stats`);const wrong = await fetch(`http://127.0.0.1:${port}/admin/stats`, { headers: { 'x-admin-token': HARDCODED_TOKEN } });const allowed = await fetch(`http://127.0.0.1:${port}/admin/stats`, { headers: { 'x-admin-token': GRADER_TOKEN } });const traversal = await fetch(`http://127.0.0.1:${port}/files?name=../secret.txt`);const nested = await fetch(`http://127.0.0.1:${port}/files?name=${encodeURIComponent('..%2f..%2fsecret.txt')}`);const legit = await fetch(`http://127.0.0.1:${port}/files?name=welcome.txt`);? await fetch(`http://127.0.0.1:${port}/p/${made.body.id}`) : null;const big = await fetch(`http://127.0.0.1:${port}/pastes`, {? await fetch(`http://127.0.0.1:${port}/pastes/${flow.body.id}`) : null;? await fetch(`http://127.0.0.1:${port}/pastes/${flow.body.id}`, {? await fetch(`http://127.0.0.1:${port}/pastes/${flow.body.id}`) : null;const http = require('node:http');const http = require('node:http');return fetch(`http://127.0.0.1:${port}${urlPath}`, {const response = await fetch(`http://127.0.0.1:${port}/deliveries/${id}`);const missing = await fetch(`http://127.0.0.1:${relayPort}/deliveries/00000000-0000-0000-0000-000000000000`);const http = require('node:http');const response = await fetch(`http://127.0.0.1:${port}/stats?${qs}`);const http = require('node:http');const post = (body) => fetch(`http://127.0.0.1:${port}/links`, {const get = (p) => fetch(`http://127.0.0.1:${port}${p}`, { redirect: 'manual' });const del = await fetch(`http://127.0.0.1:${port}/links/${code}`, { method: 'DELETE' });const post = body => fetch(`http://127.0.0.1:${port}/links`, {Why it matters: Usually legitimate, but confirm the destinations are expected and no sensitive data leaves.
Fix: Confirm the destination hosts are expected and that no sensitive data is sent off-host.
The component makes outbound network requests.
import urllib.request
req = urllib.request.Request(url, headers={"User-Agent": "aura-adapter/1.0"})with urllib.request.urlopen(req, timeout=timeout) as resp: # noqa: S310 (https only)
url = f"{base_url.rstrip('/')}/check?" + urllib.parse.urlencode({"did": did})import urllib.request
parsed = urllib.parse.urlparse(source)
return urllib.parse.urlunparse(parsed)
req = urllib.request.Request(url, headers={"User-Agent": "ECC-instinct-import/2"})with urllib.request.urlopen(req, timeout=15) as response:
import urllib.request
import urllib.request
request = requests.get((asset['modality'], request_id))
import urllib.request
import urllib.request
req = urllib.request.Request(url, data=data, headers={"Content-Type": "application/json"})with urllib.request.urlopen(req, timeout=60) as response:
Why it matters: Usually legitimate, but confirm the destinations are expected and no sensitive data leaves.
Fix: Confirm the destination hosts are expected and that no sensitive data is sent off-host.
An MCP tool surface (manifest or tool definitions) was found.
"mcpServers": {},"mcpServers": {},"tools": [
"mcpServers": {},"tools": [
"mcpServers": {},"tools": [
"mcpServers": {},"tools": [
"mcpServers": {},"tools": [
"mcpServers": {},"tools": [
"mcpServers": {},"tools": [
"mcpServers": {},"tools": [
"mcpServers": {},"tools": [
"mcpServers": {},"tools": [
"mcpServers": {},"tools": [
"mcpServers": {},"tools": [
Why it matters: Just context — review which tools it offers and their permissions.
Fix: Review the declared MCP tools and their permissions.
Check your own component
Run the same evidence-backed scan on any MCP server, agent skill, or package.
Scan your own componentHow we determine this: deterministic static analysis (regex + AST), evidence-anchored, no code execution. Methodology →