Security Analysis
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
| Column | Meaning |
|---|---|
| Issue | The vulnerability type with its rule ID (e.g. SKY-D211) |
| Severity | Risk level: Critical > High > Medium > Low |
| Message | What was found and why it's dangerous |
| Location | file:line where the issue occurs |
| Symbol | The 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_mapbuilds repo context, entrypoint hints, trust boundaries, and framework-aware file factsauditperforms whole-file security review with explicit sources, sinks, and guards in prompt contextverifyre-reviews surviving findings and records evidence such ashypothesis,review_supported, andrefuted- 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.jsoncandidates.jsonverified.jsonsummary.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
Severity: CRITICAL — Attackers can read, modify, or delete your entire database.
Skylos detects SQL injection across multiple frameworks:
| Framework | Dangerous Pattern | Rule ID |
|---|---|---|
| Raw Python | cursor.execute(f"...{user_input}...") | SKY-D210 |
| SQLAlchemy | session.execute(text(f"...{user_input}...")) | SKY-D217 |
| Django ORM | Model.objects.raw(f"...{user_input}...") | SKY-D217 |
| Pandas | pd.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
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)
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:
| Function | Risk | Rule ID | Severity |
|---|---|---|---|
eval() | Arbitrary code execution | SKY-D201 | HIGH |
exec() | Arbitrary code execution | SKY-D202 | HIGH |
pickle.load() | Deserialization attack | SKY-D203 | CRITICAL |
pickle.loads() | Deserialization attack | SKY-D204 | CRITICAL |
yaml.load() | Code execution without SafeLoader | SKY-D205 | HIGH |
hashlib.md5() | Weak cryptographic hash | SKY-D206 | MEDIUM |
hashlib.sha1() | Weak cryptographic hash | SKY-D207 | MEDIUM |
requests.get(verify=False) | SSL verification disabled | SKY-D208 | HIGH |
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
Enable secret scanning: skylos . --secrets
Skylos detects hardcoded credentials that should never be in source code:
| Provider | What's Detected | Example Pattern |
|---|---|---|
| AWS | Access keys | AKIA... |
| GitHub | Personal access tokens | ghp_..., gho_... |
| Slack | Bot and user tokens | xoxb-..., xoxp-... |
| Stripe | Live API keys | sk_live_..., rk_live_... |
| Generic | Common variable names | api_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
| Column | Meaning |
|---|---|
| Provider | The service the secret belongs to (e.g. AWS, Stripe, GitHub) or "generic" for high-entropy strings |
| Message | Description of the detected credential |
| Preview | A masked snippet of the secret |
| Location | file:line where the secret was found |
Comparison: Skylos vs. Other Tools
| Capability | Skylos | Bandit | Semgrep | Snyk Code |
|---|---|---|---|---|
| Taint analysis | ✅ | ❌ | ✅ | ✅ |
| Framework awareness | ✅ | ❌ | Partial | ✅ |
| Dead code detection | ✅ | ❌ | ❌ | ❌ |
| Quality metrics | ✅ | ❌ | ❌ | ❌ |
| AI-powered fixes | ✅ | ❌ | ❌ | ✅ |
| Self-hosted | ✅ | ✅ | ✅ | ❌ |
| Free | ✅ | ✅ | Partial | Partial |
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