Skip to main content

Security Analysis

info

Enable security scanning: skylos . --danger for static taint, dangerous-pattern, and GitHub Actions workflow checks, or skylos agent scan . --security for the verifier-backed security taskflow.

GitHub Actions Workflow Security

When run from the repository root, skylos . --danger scans GitHub Actions workflow files and local action metadata in addition to source code.

It reports CI/CD risks such as pull_request_target, broad token permissions, unpinned third-party actions, unsafe secret exposure, mutable Docker images, template injection, OIDC release jobs that invoke repo-controlled scripts, and missing timeouts on privileged jobs.

See GitHub Actions Security for workflow examples and the full rule scope.

Reading the Output

───────────────────── Security Issues ─────────────────────
# Issue Severity Message Location Symbol
1 SQL injection Critical SQL injection: tainted input api/db.py:45 handle_query
SKY-D211
2 Command injection High Command injection (shell=True) utils/run.py:23 execute_cmd
SKY-D212
ColumnMeaning
IssueThe vulnerability type with its rule ID (e.g. SKY-D211)
SeverityRisk level: Critical > High > Medium > Low
MessageWhat was found and why it's dangerous
Locationfile:line where the issue occurs
SymbolThe function or scope containing the vulnerable code

Security Taskflow Audit

skylos agent scan . --security adds a repo-aware review path on top of Skylos's static security engine:

  • repo_map builds repo context, entrypoint hints, trust boundaries, and framework-aware file facts
  • audit performs whole-file security review with explicit sources, sinks, and guards in prompt context
  • verify re-reviews surviving findings and records evidence such as hypothesis, review_supported, and refuted
  • an internal candidate ledger keeps stable finding IDs across stages so later reporting can build on the same evidence model

Example

from fastapi import FastAPI, Request
from urllib.parse import urlparse
import httpx

app = FastAPI()

@app.get("/proxy")
async def proxy(request: Request):
target = request.query_params.get("url")
return httpx.get(target).text

@app.get("/safe")
async def safe_proxy(request: Request):
target = request.query_params.get("url")
if urlparse(target).netloc not in {"internal.local"}:
target = "https://internal.local/health"
return httpx.get(target).text
skylos agent scan . --security --format json -o security.json
{
"findings": [
{
"rule_id": "SKY-D216",
"severity": "critical",
"message": "Possible SSRF: tainted URL passed to HTTP client.",
"location": {
"file": "app.py",
"line": 10
},
"symbol": "proxy",
"metadata": {
"security_evidence": "review_supported",
"review_verdict": "SUPPORTED",
"review_reason": "challenge pass resolved the SSRF flow"
}
}
],
"summary": "Found 1 issues: 1 critical"
}

The same run writes taskflow artifacts under .skylos/runs/<run-id>/:

  • repo_map.json
  • candidates.json
  • verified.json
  • summary.json

Why Pattern Matching Fails

Most security scanners use regex patterns to find dangerous code. This approach has a fundamental flaw: it can't follow data flow.

What Pattern Matchers See:

# Caught
cursor.execute("SELECT * FROM users WHERE id = " + user_id)

# Missed
def get_user(user_id):
query = build_query(user_id)
return db.execute(query)

def build_query(uid):
return f"SELECT * FROM users WHERE id = {uid}"

Pattern matchers only see the execute() call with a variable—they can't tell if that variable contains user input.

What Skylos Sees:

# Skylos traces the full data flow:

def get_user(user_id): # Tainted: function parameter
query = build_query(user_id) # Tainted: uses tainted value
return db.execute(query) # ALERT: tainted value reaches sink

def build_query(uid): # Tainted: parameter from tainted caller
return f"SELECT * FROM users WHERE id = {uid}" # Tainted string

Skylos follows the taint through every assignment and function call.


How Taint Analysis Works

Skylos builds a data flow graph for each file, tracking how values propagate from sources (user input) to sinks (dangerous functions).

The Analysis Pipeline


Vulnerability Categories

SQL Injection

warning

Severity: CRITICAL — Attackers can read, modify, or delete your entire database.

Skylos detects SQL injection across multiple frameworks:

FrameworkDangerous PatternRule ID
Raw Pythoncursor.execute(f"...{user_input}...")SKY-D210
SQLAlchemysession.execute(text(f"...{user_input}..."))SKY-D217
Django ORMModel.objects.raw(f"...{user_input}...")SKY-D217
Pandaspd.read_sql(f"...{user_input}...", conn)SKY-D217
See detection in action
# Vulnerable - Skylos catches all of these

# Direct concatenation
cursor.execute("SELECT * FROM users WHERE id = " + user_id)

# F-string interpolation
cursor.execute(f"SELECT * FROM users WHERE id = {user_id}")

# .format() method
cursor.execute("SELECT * FROM users WHERE id = {}".format(user_id))

# SQLAlchemy text()
from sqlalchemy import text
session.execute(text(f"SELECT * FROM t WHERE x = {val}"))

# Django raw()
User.objects.raw(f"SELECT * FROM users WHERE name = '{name}'")
# Safe - parameterized queries
cursor.execute("SELECT * FROM users WHERE id = %s", (user_id,))
session.execute(text("SELECT * FROM t WHERE x = :val"), {"val": val})
User.objects.raw("SELECT * FROM users WHERE name = %s", [name])

Command Injection

warning

Severity: CRITICAL — Attackers can execute arbitrary commands on your server.

See detection in action
# Vulnerable
os.system(f"convert {user_filename} output.pdf")
subprocess.run(f"echo {user_input}", shell=True)

# Safe - use list arguments, no shell
subprocess.run(["convert", user_filename, "output.pdf"])
subprocess.run(["echo", user_input]) # shell=False by default

Server-Side Request Forgery (SSRF)

warning

Severity: CRITICAL — Attackers can access internal services or cloud metadata.

# Vulnerable - user controls the URL
requests.get(user_provided_url)

# Attacker provides: http://169.254.169.254/latest/meta-data/
# -> Accesses AWS instance metadata, potentially leaking credentials

Skylos also checks Java and Go SSRF patterns. Java coverage includes servlet input flowing into URL.openStream() / openConnection(), Java HttpRequest builders, and typed RestTemplate URL calls. Go coverage includes direct http.Get style calls and http.NewRequest* construction.

Path Traversal

# Vulnerable
open(f"uploads/{user_filename}", "r")

# Attacker provides: ../../../etc/passwd
# -> Reads sensitive system files

Cross-Site Scripting (XSS)

# Vulnerable - user content rendered as HTML
from markupsafe import Markup
return Markup(f"<div>{user_comment}</div>")

Dangerous Function Detection

Beyond taint analysis, Skylos flags inherently dangerous patterns:

FunctionRiskRule IDSeverity
eval()Arbitrary code executionSKY-D201HIGH
exec()Arbitrary code executionSKY-D202HIGH
pickle.load()Deserialization attackSKY-D203CRITICAL
pickle.loads()Deserialization attackSKY-D204CRITICAL
yaml.load()Code execution without SafeLoaderSKY-D205HIGH
hashlib.md5()Weak cryptographic hashSKY-D206MEDIUM
hashlib.sha1()Weak cryptographic hashSKY-D207MEDIUM
requests.get(verify=False)SSL verification disabledSKY-D208HIGH

Webhook Signature Checks

Skylos flags webhook handlers that read inbound provider events without obvious signature verification (SKY-D282).

This is separate from normal auth checks: webhook endpoints are usually public, but the request still needs to prove it came from the provider.

# Vulnerable - trusts the webhook JSON body directly
@app.post("/stripe/webhook")
async def stripe_webhook(request):
event = await request.json()
grant_credits(event["data"]["object"]["customer"])

# Safer - verifies the provider signature first
@app.post("/stripe/webhook")
async def stripe_webhook(request):
body = await request.body()
sig = request.headers.get("stripe-signature")
event = stripe.Webhook.construct_event(body, sig, STRIPE_WEBHOOK_SECRET)

The check is intentionally conservative: it requires webhook/provider evidence, POST handling, request body usage, and no strong verification signal such as provider signature helpers or HMAC comparison.


Secret Detection

info

Enable secret scanning: skylos . --secrets

Skylos detects hardcoded credentials that should never be in source code:

ProviderWhat's DetectedExample Pattern
AWSAccess keysAKIA...
GitHubPersonal access tokensghp_..., gho_...
SlackBot and user tokensxoxb-..., xoxp-...
StripeLive API keyssk_live_..., rk_live_...
GenericCommon variable namesapi_key = "...", password = "..."
──────────────────────── Secrets ────────────────────────
# Provider Message Preview Location
1 aws AWS Access Key detected AKIA****EXAMPLE config.py:12
2 stripe Stripe Live Key detected sk_live_****xyz payments.py:8
ColumnMeaning
ProviderThe service the secret belongs to (e.g. AWS, Stripe, GitHub) or "generic" for high-entropy strings
MessageDescription of the detected credential
PreviewA masked snippet of the secret
Locationfile:line where the secret was found

Comparison: Skylos vs. Other Tools

CapabilitySkylosBanditSemgrepSnyk Code
Taint analysis
Framework awarenessPartial
Dead code detection
Quality metrics
AI-powered fixes
Self-hosted
FreePartialPartial

Integration Example

Block PRs with critical security issues:

# .github/workflows/security.yml
name: Security Scan

on: [pull_request]

jobs:
skylos-security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'

- run: pip install skylos

- name: Security Scan
run: skylos . --danger --secrets --gate

With fail_on_critical = true (default), any CRITICAL finding blocks the PR.


Next Steps

Rules Reference

See all security rules with examples

Quality Gate

Configure thresholds to block deployments