Repository navigation
Contributing
Thank you for your interest in contributing to Counterscarp Engine! This guide will help you get started with adding new features, fixing bugs, and improving the codebase.
Website: counterscarp.io
Issues: GitHub Issues
- #Development Setup
- #How to Add a New Heuristic Rule
- #How to Integrate a Custom Static Analyzer
- #How to Write Tests
- #Error Handling Conventions
- #Code Style
- #Environment Variables
- Python 3.10+ (required)
- pip for dependency management
- Git for version control
# 1. Clone the repository
git clone https://github.com/RunTimeAdmin/counterscarp.git
cd counterscarp-engine
# 2. Create a virtual environment (recommended)
python -m venv venv
# On Windows:
venv\Scripts\activate
# On macOS/Linux:
source venv/bin/activate
# 3. Install dependencies (using pyproject.toml)
pip install -e ".[dev]"
# 4. Verify installation
counterscarp-engine --help
# Or: python orchestrator.py --help# Run all tests
pytest
# Run with coverage
pytest --cov=. --cov-report=html
# Run specific test file
pytest tests/test_heuristic_scanner.py
# Run with verbose output
pytest -vHeuristic rules are defined in heuristic_scanner.py. Each rule detects a specific vulnerability pattern using regular expressions.
@dataclass
class HeuristicRule:
"""Represents a heuristic detection rule.
Attributes:
id: Unique identifier for this rule (UPPER_SNAKE_CASE).
description: Human-readable description of what this rule detects.
severity: Default severity level (CRITICAL, HIGH, MEDIUM, LOW, INFO).
pattern: Compiled regex pattern to match against code.
hint: Remediation hint for developers.
"""
id: str
description: str
severity: str
pattern: re.Pattern[str]
hint: str-
Open
heuristic_scanner.pyand locate theRULESlist (around line 77). -
Add your new rule to the list:
HeuristicRule(
id="MY_NEW_RULE",
description="Brief description of the vulnerability",
severity="HIGH", # CRITICAL, HIGH, MEDIUM, LOW, INFO
pattern=re.compile(r"your_regex_pattern_here"),
hint="Provide clear remediation guidance for developers..."
),-
Follow these guidelines:
- Use
UPPER_SNAKE_CASEfor rule IDs - Severity should reflect real-world exploit impact
- Patterns should be specific enough to avoid false positives
- Hints should include actionable fix suggestions
- Use
Simple Pattern Match:
HeuristicRule(
id="TX_ORIGIN_USAGE",
description="Use of tx.origin (dangerous for auth checks)",
severity="HIGH",
pattern=re.compile(r"tx\.origin"),
hint="Avoid tx.origin for authorization; use msg.sender and proper role-based access control.",
),Complex Pattern with Lookahead:
HeuristicRule(
id="ORACLE_STALENESS_CHECK",
description="Chainlink oracle without staleness/validity check",
severity="CRITICAL",
pattern=re.compile(r"(latestAnswer|latestRoundData)\(\)(?!.*require.*updatedAt|.*block\.timestamp)"),
hint="CRITICAL: Check updatedAt timestamp and answeredInRound to prevent stale price attacks.",
)The scanner automatically filters out matches that occur in:
- Single-line comments (
//) - Multi-line comments (
/* */) - String literals (
"..."or'...')
This is handled by the is_in_code_context() and is_in_multiline_comment() functions. You don't need to modify these unless you're adding new context types.
# Test the rule manually
python heuristic_scanner.py /path/to/test/contracts --config counterscarp-pr.toml
# Or write a unit test
def test_my_new_rule():
test_code = "// vulnerable code with tx.origin"
findings = scan_file("test.sol", config=None)
assert any(f.rule_id == "MY_NEW_RULE" for f in findings)You can integrate external static analysis tools by following the wrapper pattern used by existing analyzers like aderyn_wrapper.py.
-
Create a new wrapper file (e.g.,
my_analyzer_wrapper.py):
#!/usr/bin/env python3
"""Wrapper for My Custom Analyzer."""
from __future__ import annotations
import subprocess
import json
from typing import Dict, Any, List
from logger import get_logger
from exceptions import (
CounterscarperAnalysisError,
CounterscarperToolNotFoundError,
CounterscarperTimeoutError,
)
logger = get_logger(__name__)
def check_tool_installed() -> bool:
"""Check if the analyzer is available."""
try:
result = subprocess.run(
["my-analyzer", "--version"],
capture_output=True,
text=True,
timeout=5
)
return result.returncode == 0
except FileNotFoundError:
return False
def run_my_analyzer(project_root: str) -> Dict[str, Any]:
"""Run the analyzer and return structured results."""
if not check_tool_installed():
raise CounterscarperToolNotFoundError(
"my-analyzer not found",
details={"tool": "my-analyzer", "install_cmd": "pip install my-analyzer"}
)
cmd = ["my-analyzer", project_root, "--output", "json"]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=120
)
return json.loads(result.stdout)
except subprocess.TimeoutExpired as e:
raise CounterscarperTimeoutError(
"Analysis timed out",
details={"operation": "my_analyzer", "timeout_seconds": 120}
) from e
except Exception as e:
raise CounterscarperAnalysisError(
"Analysis failed",
details={"error": str(e)}
) from e-
Add graceful import fallback in
orchestrator.py:
# In orchestrator.py, add near other optional imports (around line 27)
try:
import my_analyzer_wrapper
logger.debug("My analyzer wrapper imported successfully")
except ImportError as e:
logger.info(f"My analyzer not available: {e}")
my_analyzer_wrapper = None- Integrate into the analysis pipeline:
# In orchestrator.py main(), add a new phase:
# [PHASE X] My Custom Analyzer
if args.my_analyzer and os.path.isdir(args.target):
print("\n>>> Running My Custom Analyzer...")
if my_analyzer_wrapper is None:
print("[!] My analyzer not available.")
else:
try:
my_results = my_analyzer_wrapper.run_my_analyzer(args.target)
except Exception:
print("[!] My analyzer failed; continuing without results.")
my_results = {"error": "Analysis failed"}Your wrapper should return a dictionary with this structure:
{
"findings": [
{
"rule_id": "RULE_NAME",
"severity": "CRITICAL", # CRITICAL, HIGH, MEDIUM, LOW, INFO
"title": "Short description",
"description": "Detailed explanation",
"file": "path/to/file.sol",
"line_no": 42,
"code_snippet": "optional code excerpt"
}
],
"summary": {
"total": 1,
"critical": 1,
"high": 0,
# ... etc
}
}counterscarp-engine/
├── tests/
│ ├── __init__.py
│ ├── conftest.py # Shared fixtures
│ ├── test_access_matrix.py
│ ├── test_config_loader.py
│ ├── test_exceptions.py
│ ├── test_heuristic_scanner.py
│ ├── test_intent_check.py
│ ├── test_orchestrator.py
│ ├── test_red_team_scan.py
│ ├── test_report_generator.py
│ ├── test_supply_chain_check.py
│ └── test_upgrade_diff.py
└── test_fixtures/ # Optional: test contract files
├── vulnerable_contract.sol
└── safe_contract.sol
Create a tests/conftest.py file with common fixtures:
"""Shared test fixtures for Counterscarp Engine."""
import pytest
import tempfile
import os
@pytest.fixture
def temp_sol_file():
"""Create a temporary Solidity file for testing."""
with tempfile.NamedTemporaryFile(
mode='w',
suffix='.sol',
delete=False
) as f:
f.write("""
pragma solidity ^0.8.0;
contract Test {
function vulnerable() public {
// Code with known vulnerability
tx.origin.call{value: 1 ether}("");
}
}
""")
temp_path = f.name
yield temp_path
# Cleanup
os.unlink(temp_path)
@pytest.fixture
def mock_config():
"""Return a mock configuration object."""
from unittest.mock import MagicMock
config = MagicMock()
config.heuristics.enabled = True
config.heuristics.disabled_rules = []
config.heuristics.get_rule_severity.return_value = "HIGH"
config.is_finding_suppressed.return_value = None
return configWhen testing code that calls external tools (Slither, Aderyn, etc.), use mocking:
import pytest
from unittest.mock import patch, MagicMock
def test_slither_wrapper_mocked():
"""Test slither wrapper with mocked subprocess."""
mock_result = MagicMock()
mock_result.returncode = 0
mock_result.stdout = '{"results": {"detectors": []}}'
mock_result.stderr = ''
with patch('subprocess.run', return_value=mock_result):
from red_team_scan import run_slither
results = run_slither('./test_contracts')
assert results == []
def test_aderyn_not_installed():
"""Test behavior when Aderyn is not installed."""
with patch('subprocess.run', side_effect=FileNotFoundError()):
from aderyn_wrapper import check_aderyn_installed
assert check_aderyn_installed() is Falsedef test_tx_origin_detection(temp_sol_file, mock_config):
"""Test that tx.origin usage is detected."""
from heuristic_scanner import scan_file
findings = scan_file(temp_sol_file, mock_config)
tx_origin_findings = [f for f in findings if f.rule_id == "TX_ORIGIN_USAGE"]
assert len(tx_origin_findings) > 0
assert tx_origin_findings[0].severity == "HIGH"All exceptions are defined in exceptions.py:
CounterscarperError (base)
├── CounterscarperConfigError # Configuration issues
├── CounterscarperAnalysisError # Analyzer failures
├── CounterscarperAPIError # External API failures
├── CounterscarperReportError # Report generation failures
├── CounterscarperToolNotFoundError # Missing external tools
├── CounterscarperValidationError # Input validation failures
└── CounterscarperTimeoutError # Operation timeouts
| Exception | Use When | Example |
|---|---|---|
CounterscarperConfigError |
TOML syntax error, invalid config value | Missing [engine] section |
CounterscarperAnalysisError |
Slither/Aderyn/Mythril fails | Compilation error, timeout |
CounterscarperAPIError |
OSV.dev or threat intel API fails | 500 error from API |
CounterscarperReportError |
Can't write report file | Permission denied on output dir |
CounterscarperToolNotFoundError |
External tool not installed | Slither not in PATH |
CounterscarperValidationError |
User input is invalid | Invalid contract address format |
CounterscarperTimeoutError |
Operation exceeds time limit | Mythril analysis > 5 minutes |
When adding new API integrations (threat intelligence, external services), use the resilient HTTP utilities:
from http_utils import resilient_get, resilient_post
# Automatic retry with exponential backoff
response = resilient_get(
"https://api.example.com/data",
timeout=30,
max_retries=3
)
# Rate-limited POST requests
response = resilient_post(
"https://api.example.com/submit",
json={"key": "value"},
rate_limit=5 # requests per second
)Features:
- Exponential backoff with jitter for failed requests
- Rate limiting to prevent API quota exhaustion
- Automatic retry on 5xx errors and timeouts
-
Respects
Retry-Afterheaders for rate limiting
Use the centralized logger from logger.py:
from logger import get_logger
logger = get_logger(__name__)
# Different log levels:
logger.debug("Detailed debugging info") # Development only
logger.info("General operational info") # Normal operation
logger.warning("Potential issue detected") # Non-critical problem
logger.error("Error occurred") # Something failed
logger.critical("Critical failure") # May crash the programBest Practices:
- Use
debugfor detailed tracing (regex matches, internal state) - Use
infofor milestones ("Scan started", "Report generated") - Use
warningfor recoverable issues ("Tool not found, skipping") - Use
errorfor failures that affect output quality - Use
criticalfor failures that prevent any output
Always chain exceptions to preserve context:
from exceptions import CounterscarperConfigError
try:
load_config(path)
except Exception as e:
raise CounterscarperConfigError(
"Failed to load configuration",
details={"path": path, "error": str(e)}
) from eAll public APIs must have type hints:
from typing import List, Dict, Optional, Any
def scan_target(
target: str,
config: Optional[CounterscarperConfig] = None
) -> List[HeuristicFinding]:
"""Scan a target file or directory.
Args:
target: Path to .sol file or directory.
config: Optional configuration object.
Returns:
List of heuristic findings.
"""
...Use Google-style docstrings for all functions and classes:
def calculate_risk_score(findings: List[Finding]) -> float:
"""Calculate overall risk score (0-100) based on finding severity.
Formula: weighted_sum / max_possible_weight
Args:
findings: List of findings to calculate score from.
Returns:
Risk score between 0 and 100.
Example:
>>> findings = [Finding(severity="CRITICAL", ...)]
>>> score = calculate_risk_score(findings)
>>> print(f"Risk: {score}/100")
"""Every Python file must include this import at the top:
#!/usr/bin/env python3
"""Module docstring."""
from __future__ import annotations
import os
import re
from typing import List, Optional
# Your code hereThis enables:
- Postponed evaluation of annotations (PEP 563)
- Use of forward references without quotes
- Better performance for type checking
The following environment variables are supported:
| Variable | Description | Default | Required |
|---|---|---|---|
COUNTERSCARP_LOG_LEVEL |
Log level: DEBUG, INFO, WARNING, ERROR, CRITICAL | INFO | No |
COUNTERSCARP_LOG_FORMAT |
Output format: "text" or "json" | text | No |
COUNTERSCARP_LOG_FILE |
Path to log file (optional) | (none) | No |
OPENAI_API_KEY |
OpenAI API key for exploit generation | (none) | For AI features |
# Debug logging to file
export COUNTERSCARP_LOG_LEVEL=DEBUG
export COUNTERSCARP_LOG_FILE=/var/log/counterscarp.log
python orchestrator.py --target ./contracts
# JSON format for structured logging
export COUNTERSCARP_LOG_FORMAT=json
python orchestrator.py --target ./contracts 2>&1 | jq
# OpenAI for exploit generation
export OPENAI_API_KEY="sk-..."
python exploit_generator.py --finding-json findings.json- Fork the repository and create a feature branch
- Make your changes following the guidelines above
- Add tests for new functionality
- Run the test suite to ensure nothing is broken
- Update documentation if needed
- Submit a pull request with a clear description
- Code follows the style guidelines
- Type hints on all public APIs
- Google-style docstrings added/updated
-
from __future__ import annotationsat top of file - Tests added for new functionality
- All tests pass (
pytest) - Documentation updated (README, CONTRIBUTING, etc.)
- No hardcoded paths or credentials
- Open an issue on GitHub
- Visit counterscarp.io
- Contact: contact@counterscarp.io
Thank you for contributing to Counterscarp Engine!
- Architecture — System architecture and data flows
- Plugin-Development — Creating plugins and extensions
- Rules-Catalog — Security rules reference
Scarpshield Wiki (Counterscarp Engine)
- Home
- Current Status
- Getting Started
- CLI Reference
- Configuration
- Web App Guide
- Deployment
- Report Formats
- Rules Catalog
- Architecture
- Plugin Development
- Contributing
- Security & Licensing
- Pricing & Pro Features
- FAQ & Troubleshooting
GitHub Repo | Web App | PyPI