* chore: Exclude CLAUDE.md from Cargo.toml

* feat: add callgraph module and integrate into main analysis flow

* feat: enhance CLI with new severity filtering and analysis modes

* feat: update CHANGELOG with recent enhancements and fixes to severity filtering and output handling

* feat: implement state-model dataflow analysis for resource lifecycle and auth state

* feat: enhance diagnostic output formatting and add evidence structure

* feat: implement attack surface ranking for diagnostics with scoring and sorting

* feat: add comprehensive documentation for installation, usage, and rules reference

* feat: add multiple language support for command execution and evaluation endpoints

* feat: implement inline suppression for findings using `nyx:ignore` comments

* feat: add confidence levels to AST patterns and update output structure

* feat: implement low-noise prioritization system with category filtering, rollup grouping, and configurable budgets

* feat: bump version to 0.4.0 and update changelog with new features and improvements

* feat: add dead code allowances to various functions in mod.rs and real_world_tests.rs
This commit is contained in:
Eli Peter 2026-02-25 21:16:36 -05:00 committed by GitHub
parent 19b578c5c4
commit 1bbe4b1cfb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
456 changed files with 25628 additions and 1228 deletions

View file

@ -7,11 +7,13 @@ use std::path::Path;
// ── Deterministic test config ──────────────────────────────────────────────
#[allow(dead_code)]
pub fn test_config(mode: AnalysisMode) -> Config {
let mut cfg = Config::default();
cfg.scanner.mode = mode;
cfg.scanner.read_vcsignore = false;
cfg.scanner.require_git_to_read_vcsignore = false;
cfg.scanner.enable_state_analysis = true;
cfg.performance.worker_threads = Some(1);
cfg.performance.batch_size = 64;
cfg.performance.channel_multiplier = 1;
@ -21,6 +23,7 @@ pub fn test_config(mode: AnalysisMode) -> Config {
// ── Scan helpers ───────────────────────────────────────────────────────────
/// Full two-pass scan of a directory (filesystem only, no index).
#[allow(dead_code)]
pub fn scan_fixture_dir(path: &Path, mode: AnalysisMode) -> Vec<Diag> {
let cfg = test_config(mode);
nyx_scanner::scan_no_index(path, &cfg).expect("scan_no_index should succeed")
@ -28,10 +31,12 @@ pub fn scan_fixture_dir(path: &Path, mode: AnalysisMode) -> Vec<Diag> {
// ── Counting / assertion helpers ───────────────────────────────────────────
#[allow(dead_code)]
pub fn count_by_prefix(diags: &[Diag], prefix: &str) -> usize {
diags.iter().filter(|d| d.id.starts_with(prefix)).count()
}
#[allow(dead_code)]
pub fn assert_min_findings(diags: &[Diag], prefix: &str, min: usize) {
let count = count_by_prefix(diags, prefix);
assert!(
@ -52,6 +57,7 @@ pub fn assert_min_findings(diags: &[Diag], prefix: &str, min: usize) {
);
}
#[allow(dead_code)]
pub fn assert_no_findings(diags: &[Diag], prefix: &str) {
let matching: Vec<_> = diags.iter().filter(|d| d.id.starts_with(prefix)).collect();
assert!(
@ -65,6 +71,7 @@ pub fn assert_no_findings(diags: &[Diag], prefix: &str) {
);
}
#[allow(dead_code)]
pub fn assert_max_findings(diags: &[Diag], max_total: usize, max_high: usize) {
let high_count = diags
.iter()
@ -130,6 +137,7 @@ pub struct PerformanceExpectations {
}
/// Load and parse `expectations.json` from a fixture directory.
#[allow(dead_code)]
pub fn load_expectations(fixture_dir: &Path) -> Expectations {
let path = fixture_dir.join("expectations.json");
let content = std::fs::read_to_string(&path)
@ -139,6 +147,7 @@ pub fn load_expectations(fixture_dir: &Path) -> Expectations {
}
/// Validate a set of diagnostics against a fixture's expectations.json.
#[allow(dead_code)]
pub fn validate_expectations(diags: &[Diag], fixture_dir: &Path) {
let exp = load_expectations(fixture_dir);

View file

@ -1,12 +1,12 @@
{
"required_findings": [
{ "id_prefix": "taint-unsanitised-flow", "min_count": 4 },
{ "id_prefix": "strcpy_call", "min_count": 1 },
{ "id_prefix": "strcat_call", "min_count": 1 },
{ "id_prefix": "sprintf_call", "min_count": 4 },
{ "id_prefix": "gets_call", "min_count": 1 },
{ "id_prefix": "scanf_with_percent_s", "min_count": 1 },
{ "id_prefix": "system_call", "min_count": 3 },
{ "id_prefix": "c.memory.strcpy", "min_count": 1 },
{ "id_prefix": "c.memory.strcat", "min_count": 1 },
{ "id_prefix": "c.memory.sprintf", "min_count": 4 },
{ "id_prefix": "c.memory.gets", "min_count": 1 },
{ "id_prefix": "c.memory.scanf_percent_s", "min_count": 1 },
{ "id_prefix": "c.cmdi.system", "min_count": 3 },
{ "id_prefix": "cfg-unguarded-sink", "min_count": 5 }
],
"forbidden_findings": [],

View file

@ -1,10 +1,10 @@
{
"required_findings": [
{ "id_prefix": "taint-unsanitised-flow", "min_count": 6 },
{ "id_prefix": "eval_call", "min_count": 1 },
{ "id_prefix": "document_write", "min_count": 1 },
{ "id_prefix": "settimeout_string", "min_count": 1 },
{ "id_prefix": "cookie_assignment", "min_count": 1 }
{ "id_prefix": "js.code_exec.eval", "min_count": 1 },
{ "id_prefix": "js.xss.document_write", "min_count": 1 },
{ "id_prefix": "js.code_exec.settimeout_string", "min_count": 1 },
{ "id_prefix": "js.xss.cookie_write", "min_count": 1 }
],
"forbidden_findings": [],
"noise_budget": {

View file

@ -1,13 +1,13 @@
{
"required_findings": [
{ "id_prefix": "taint-unsanitised-flow", "min_count": 8 },
{ "id_prefix": "eval_call", "min_count": 1 },
{ "id_prefix": "exec_call", "min_count": 2 },
{ "id_prefix": "cfg-auth-gap", "min_count": 5 }
{ "id_prefix": "py.code_exec.eval", "min_count": 1 },
{ "id_prefix": "py.code_exec.exec", "min_count": 2 },
{ "id_prefix": "state-unauthed-access", "min_count": 5 }
],
"forbidden_findings": [],
"noise_budget": {
"max_total_findings": 35,
"max_total_findings": 50,
"max_high_findings": 25
},
"performance_expectations": {

View file

@ -1,7 +1,7 @@
{
"required_findings": [
{ "id_prefix": "taint-unsanitised-flow", "min_count": 4 },
{ "id_prefix": "exec_command", "min_count": 3 },
{ "id_prefix": "go.cmdi.exec_command", "min_count": 3 },
{ "id_prefix": "cfg-unguarded-sink", "min_count": 1 }
],
"forbidden_findings": [],

View file

@ -1,14 +1,14 @@
{
"required_findings": [
{ "id_prefix": "taint-unsanitised-flow", "min_count": 2 },
{ "id_prefix": "runtime_exec", "min_count": 2 },
{ "id_prefix": "class_for_name", "min_count": 1 },
{ "id_prefix": "cfg-unguarded-sink", "min_count": 2 }
{ "id_prefix": "java.cmdi.runtime_exec", "min_count": 2 },
{ "id_prefix": "java.reflection.class_forname", "min_count": 1 },
{ "id_prefix": "cfg-unguarded-sink", "min_count": 1 }
],
"forbidden_findings": [],
"noise_budget": {
"max_total_findings": 15,
"max_high_findings": 8
"max_total_findings": 20,
"max_high_findings": 12
},
"performance_expectations": {
"max_ms_no_index": 1000,

View file

@ -1,10 +1,7 @@
{
"required_findings": [
{ "id_prefix": "taint-unsanitised-flow", "min_count": 10 },
{ "id_prefix": "eval_call", "min_count": 2 },
{ "id_prefix": "unwrap_call", "min_count": 3 },
{ "id_prefix": "expect_call", "min_count": 1 },
{ "id_prefix": "panic_macro", "min_count": 1 },
{ "id_prefix": "js.code_exec.eval", "min_count": 1 },
{ "id_prefix": "cfg-unguarded-sink", "min_count": 2 }
],
"forbidden_findings": [],

24
tests/fixtures/patterns/c/negative.c vendored Normal file
View file

@ -0,0 +1,24 @@
/* Negative fixture: none of these should trigger security patterns. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void safe_snprintf(const char *name) {
char buf[128];
snprintf(buf, sizeof(buf), "Hello %s", name);
}
void safe_strncpy(const char *src) {
char dst[32];
strncpy(dst, src, sizeof(dst) - 1);
dst[sizeof(dst) - 1] = '\0';
}
void safe_fgets() {
char buf[64];
fgets(buf, sizeof(buf), stdin);
}
void safe_printf_literal() {
printf("Hello %s\n", "world");
}

50
tests/fixtures/patterns/c/positive.c vendored Normal file
View file

@ -0,0 +1,50 @@
/* Positive fixture: each snippet should trigger the named pattern. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/* c.memory.gets */
void trigger_gets() {
char buf[64];
gets(buf);
}
/* c.memory.strcpy */
void trigger_strcpy(char *src) {
char dst[32];
strcpy(dst, src);
}
/* c.memory.strcat */
void trigger_strcat(char *extra) {
char buf[64] = "prefix";
strcat(buf, extra);
}
/* c.memory.sprintf */
void trigger_sprintf(const char *name) {
char buf[128];
sprintf(buf, "Hello %s", name);
}
/* c.memory.scanf_percent_s */
void trigger_scanf() {
char name[32];
scanf("%s", name);
}
/* c.cmdi.system */
void trigger_system(const char *cmd) {
system(cmd);
}
/* c.cmdi.popen */
void trigger_popen(const char *cmd) {
FILE *f = popen(cmd, "r");
pclose(f);
}
/* c.memory.printf_no_fmt */
void trigger_printf_no_fmt(char *user_data) {
printf(user_data);
}

View file

@ -0,0 +1,24 @@
// Negative fixture: none of these should trigger security patterns.
#include <cstdio>
#include <cstring>
#include <string>
void safe_string_ops() {
std::string s = "hello";
std::string copy = s;
auto len = s.length();
}
void safe_cast() {
double d = 3.14;
int i = static_cast<int>(d);
}
void safe_snprintf(const char *name) {
char buf[128];
snprintf(buf, sizeof(buf), "Hello %s", name);
}
void safe_printf_literal() {
printf("Hello %s\n", "world");
}

View file

@ -0,0 +1,49 @@
// Positive fixture: each snippet should trigger the named pattern.
#include <cstdlib>
#include <cstring>
#include <cstdio>
// cpp.memory.gets
void trigger_gets() {
char buf[64];
gets(buf);
}
// cpp.memory.strcpy
void trigger_strcpy(const char *src) {
char dst[32];
strcpy(dst, src);
}
// cpp.memory.strcat
void trigger_strcat(const char *extra) {
char buf[64] = "prefix";
strcat(buf, extra);
}
// cpp.memory.sprintf
void trigger_sprintf(const char *name) {
char buf[128];
sprintf(buf, "Hello %s", name);
}
// cpp.cmdi.system
void trigger_system(const char *cmd) {
system(cmd);
}
// cpp.memory.reinterpret_cast
void trigger_reinterpret_cast() {
int x = 42;
float *fp = reinterpret_cast<float*>(&x);
}
// cpp.memory.const_cast
void trigger_const_cast(const int *p) {
int *q = const_cast<int*>(p);
}
// cpp.memory.printf_no_fmt
void trigger_printf_no_fmt(char *user_data) {
printf(user_data);
}

23
tests/fixtures/patterns/go/negative.go vendored Normal file
View file

@ -0,0 +1,23 @@
package main
import (
"crypto/sha256"
"database/sql"
)
func safeHash(data []byte) {
sha256.Sum256(data)
}
func safeParamQuery(db *sql.DB, user string) {
db.Query("SELECT * FROM users WHERE name = $1", user)
}
func safeLiteralQuery(db *sql.DB) {
db.Query("SELECT COUNT(*) FROM users")
}
func safeStringOps() {
x := "hello"
_ = len(x)
}

55
tests/fixtures/patterns/go/positive.go vendored Normal file
View file

@ -0,0 +1,55 @@
package main
import (
"crypto/md5"
"crypto/sha1"
"database/sql"
"encoding/gob"
"os"
"os/exec"
"unsafe"
)
// go.cmdi.exec_command
func triggerExecCommand(cmd string) {
exec.Command("bash", "-c", cmd)
}
// go.memory.unsafe_pointer
func triggerUnsafePointer() {
x := 42
p := unsafe.Pointer(&x)
_ = p
}
// go.transport.insecure_skip_verify
func triggerInsecureSkipVerify() {
_ = struct{ InsecureSkipVerify bool }{InsecureSkipVerify: true}
}
// go.crypto.md5
func triggerMD5(data []byte) {
md5.Sum(data)
}
// go.crypto.sha1
func triggerSHA1(data []byte) {
sha1.Sum(data)
}
// go.sqli.query_concat
func triggerSQLConcat(db *sql.DB, user string) {
db.Query("SELECT * FROM users WHERE name = '" + user + "'")
}
// go.secrets.hardcoded_key
func triggerHardcodedSecret() {
password := "super_secret_password_12345"
_ = password
}
// go.deser.gob_decode
func triggerGobDecode(f *os.File) {
dec := gob.NewDecoder(f)
_ = dec
}

View file

@ -0,0 +1,22 @@
import java.sql.*;
import java.security.SecureRandom;
class Negative {
// Safe: parameterized query
void safeQuery(Connection conn, String user) throws Exception {
PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE name = ?");
ps.setString(1, user);
ResultSet rs = ps.executeQuery();
}
// Safe: SecureRandom instead of Random
void safeRandom() {
SecureRandom sr = new SecureRandom();
int token = sr.nextInt();
}
// Safe: no concatenation in SQL
void safeLiteralQuery(Statement stmt) throws Exception {
stmt.executeQuery("SELECT COUNT(*) FROM users");
}
}

View file

@ -0,0 +1,48 @@
import java.io.*;
import java.util.Random;
import java.security.MessageDigest;
class Positive {
// java.deser.readobject
void triggerDeser(InputStream is) throws Exception {
ObjectInputStream ois = new ObjectInputStream(is);
Object obj = ois.readObject();
}
// java.cmdi.runtime_exec
void triggerRuntimeExec(String cmd) throws Exception {
Runtime.getRuntime().exec(cmd);
}
// java.reflection.class_forname
void triggerClassForName(String name) throws Exception {
Class.forName(name);
}
// java.reflection.method_invoke
void triggerMethodInvoke(Object target) throws Exception {
java.lang.reflect.Method m = target.getClass().getMethod("run");
m.invoke(target);
}
// java.sqli.execute_concat
void triggerSqlConcat(java.sql.Statement stmt, String user) throws Exception {
stmt.executeQuery("SELECT * FROM users WHERE name = '" + user + "'");
}
// java.crypto.insecure_random
void triggerInsecureRandom() {
Random r = new Random();
int token = r.nextInt();
}
// java.crypto.weak_digest
void triggerWeakDigest() throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
}
// java.xss.getwriter_print
void triggerGetWriterPrint(javax.servlet.http.HttpServletResponse resp) throws Exception {
resp.getWriter().println("<html>" + "data" + "</html>");
}
}

View file

@ -0,0 +1,25 @@
// Negative fixture: none of these should trigger security patterns.
function safeStringOps() {
var x = "hello";
var y = x.toUpperCase();
var z = JSON.stringify({ key: "value" });
}
function safeTimeout(fn) {
// Function reference, not string
setTimeout(fn, 1000);
}
function safeDomManipulation(el) {
el.textContent = "safe text";
el.setAttribute("class", "active");
}
function safeRandomness() {
var buf = crypto.getRandomValues(new Uint8Array(16));
}
function safeCopy(src) {
var copy = Object.assign({}, src);
}

View file

@ -0,0 +1,51 @@
// Positive fixture: each snippet should trigger the named pattern.
// js.code_exec.eval
function triggerEval(code) {
eval(code);
}
// js.code_exec.new_function
function triggerNewFunction(body) {
var fn = new Function(body);
}
// js.code_exec.settimeout_string
function triggerSetTimeout() {
setTimeout("alert(1)", 1000);
}
// js.xss.document_write
function triggerDocumentWrite(data) {
document.write(data);
}
// js.xss.outer_html
function triggerOuterHtml(el, data) {
el.outerHTML = data;
}
// js.xss.insert_adjacent_html
function triggerInsertAdjacentHtml(el, data) {
el.insertAdjacentHTML("beforeend", data);
}
// js.prototype.proto_assignment
function triggerProtoAssignment(obj) {
obj.__proto__ = { malicious: true };
}
// js.xss.location_assign
function triggerLocationAssign(url) {
window.location = url;
}
// js.xss.cookie_write
function triggerCookieWrite(sid) {
document.cookie = "session=" + sid;
}
// js.crypto.math_random
function triggerMathRandom() {
var token = Math.random();
}

View file

@ -0,0 +1,25 @@
<?php
// Negative fixture: none of these should trigger security patterns.
function safe_query($pdo, $user) {
$stmt = $pdo->prepare("SELECT * FROM users WHERE name = ?");
$stmt->execute([$user]);
}
function safe_hash($data) {
return hash("sha256", $data);
}
function safe_random() {
return random_int(1, 100);
}
function safe_include() {
include "config.php";
}
function safe_string_ops() {
$x = "hello";
$y = strtoupper($x);
$z = strlen($y);
}

View file

@ -0,0 +1,57 @@
<?php
// Positive fixture: each snippet should trigger the named pattern.
// php.code_exec.eval
function trigger_eval($code) {
eval($code);
}
// php.code_exec.create_function
function trigger_create_function() {
$fn = create_function('$a', 'return $a * 2;');
}
// php.code_exec.preg_replace_e
function trigger_preg_replace_e($input) {
preg_replace('/test/e', 'strtoupper("$1")', $input);
}
// php.code_exec.assert_string
function trigger_assert($code) {
assert("strlen('$code') > 0");
}
// php.cmdi.system
function trigger_system($cmd) {
system($cmd);
}
// php.deser.unserialize
function trigger_unserialize($data) {
unserialize($data);
}
// php.sqli.query_concat
function trigger_sql_concat($user) {
mysql_query("SELECT * FROM users WHERE name = '" . $user . "'");
}
// php.path.include_variable
function trigger_include($path) {
include $path;
}
// php.crypto.md5
function trigger_md5($data) {
md5($data);
}
// php.crypto.sha1
function trigger_sha1($data) {
sha1($data);
}
// php.crypto.rand
function trigger_rand() {
$r = rand();
}

View file

@ -0,0 +1,23 @@
# Negative fixture: none of these should trigger security patterns.
import subprocess
import hashlib
def safe_subprocess():
# No shell=True
subprocess.run(["ls", "-la"])
def safe_hash():
hashlib.sha256(b"data")
def safe_literal_query(cursor):
cursor.execute("SELECT COUNT(*) FROM users")
def safe_yaml_load(data):
import yaml
yaml.safe_load(data)
def safe_string_ops():
x = "hello"
y = x.upper()
z = len(y)

View file

@ -0,0 +1,51 @@
# Positive fixture: each snippet should trigger the named pattern.
import os
import subprocess
import pickle
import yaml
import hashlib
# py.code_exec.eval
def trigger_eval(data):
result = eval(data)
# py.code_exec.exec
def trigger_exec(code):
exec(code)
# py.code_exec.compile
def trigger_compile(code):
co = compile(code, "<string>", "exec")
# py.cmdi.os_system
def trigger_os_system(cmd):
os.system(cmd)
# py.cmdi.os_popen
def trigger_os_popen(cmd):
os.popen(cmd)
# py.cmdi.subprocess_shell
def trigger_subprocess_shell(cmd):
subprocess.run(cmd, shell=True)
# py.deser.pickle_loads
def trigger_pickle(data):
obj = pickle.loads(data)
# py.deser.yaml_load
def trigger_yaml(data):
obj = yaml.load(data)
# py.sqli.execute_format
def trigger_sql_concat(cursor, user):
cursor.execute("SELECT * FROM users WHERE name = '" + user + "'")
# py.crypto.md5
def trigger_md5(data):
hashlib.md5(data)
# py.crypto.sha1
def trigger_sha1(data):
hashlib.sha1(data)

View file

@ -0,0 +1,25 @@
# Negative fixture: none of these should trigger security patterns.
def safe_yaml(data)
YAML.safe_load(data)
end
def safe_system
Dir.entries(".")
end
def safe_send(obj)
obj.send(:to_s)
end
def safe_open
File.open("config.yml", "r") do |f|
f.read
end
end
def safe_string_ops
x = "hello"
y = x.upcase
z = y.length
end

View file

@ -0,0 +1,51 @@
# Positive fixture: each snippet should trigger the named pattern.
# rb.code_exec.eval
def trigger_eval(code)
eval(code)
end
# rb.code_exec.instance_eval
def trigger_instance_eval(obj, code)
obj.instance_eval(code)
end
# rb.code_exec.class_eval
def trigger_class_eval(klass, code)
klass.class_eval(code)
end
# rb.cmdi.backtick
def trigger_backtick
`uname -a`
end
# rb.cmdi.system_interp
def trigger_system_interp(cmd)
system("run #{cmd}")
end
# rb.deser.yaml_load
def trigger_yaml_load(data)
YAML.load(data)
end
# rb.deser.marshal_load
def trigger_marshal_load(data)
Marshal.load(data)
end
# rb.reflection.send_dynamic
def trigger_send_dynamic(obj, method_name)
obj.send(method_name)
end
# rb.reflection.constantize
def trigger_constantize(name)
name.constantize
end
# rb.ssrf.open_uri
def trigger_open_uri
open("https://example.com/api")
end

View file

@ -0,0 +1,36 @@
// Negative fixture: none of the security-relevant patterns should fire here.
fn safe_option_handling() {
let x: Option<i32> = Some(1);
// Using match instead of unwrap
match x {
Some(v) => println!("{}", v),
None => println!("none"),
}
}
fn safe_result_handling() -> Result<(), String> {
let x: Result<i32, String> = Ok(1);
// Using ? instead of unwrap
let _v = x?;
Ok(())
}
fn safe_copy() {
let src = vec![1, 2, 3];
let mut dst = vec![0; 3];
// Safe copy via clone
dst.clone_from(&src);
}
fn safe_cast() {
let x: u32 = 42;
// Widening cast is fine
let _ = x as u64;
}
fn safe_string_ops() {
let s = String::from("hello");
let _ = s.len();
let _ = s.is_empty();
}

View file

@ -0,0 +1,78 @@
// Positive fixture: each snippet should trigger the named pattern.
use std::mem;
use std::ptr;
// rs.memory.transmute
fn trigger_transmute() {
let x: u32 = unsafe { mem::transmute(1.0f32) };
let _ = x;
}
// rs.memory.copy_nonoverlapping
fn trigger_copy_nonoverlapping() {
let src = [1u8; 4];
let mut dst = [0u8; 4];
unsafe { ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr(), 4) };
}
// rs.memory.get_unchecked
fn trigger_get_unchecked() {
let v = vec![1, 2, 3];
let _ = unsafe { v.get_unchecked(0) };
}
// rs.memory.mem_zeroed
fn trigger_mem_zeroed() {
let _: u64 = unsafe { mem::zeroed() };
}
// rs.memory.ptr_read
fn trigger_ptr_read() {
let x = 42u32;
let _ = unsafe { ptr::read(&x) };
}
// rs.quality.unsafe_block
fn trigger_unsafe_block() {
unsafe {
let _ = 1;
}
}
// rs.quality.unsafe_fn
unsafe fn trigger_unsafe_fn() {}
// rs.quality.unwrap
fn trigger_unwrap() {
let x: Option<i32> = Some(1);
let _ = x.unwrap();
}
// rs.quality.expect
fn trigger_expect() {
let x: Option<i32> = Some(1);
let _ = x.expect("should exist");
}
// rs.quality.panic_macro
fn trigger_panic() {
panic!("boom");
}
// rs.quality.todo
fn trigger_todo() {
todo!();
}
// rs.memory.narrow_cast
fn trigger_narrow_cast() {
let big: u32 = 1000;
let _ = big as u8;
}
// rs.memory.mem_forget
fn trigger_mem_forget() {
let v = vec![1, 2, 3];
mem::forget(v);
}

View file

@ -0,0 +1,25 @@
// Negative fixture: none of the security-relevant patterns should fire here.
function safeStringOps(): string {
const x: string = "hello";
return x.toUpperCase();
}
function safeTimeout(fn: () => void): void {
setTimeout(fn, 1000);
}
function safeDomManipulation(el: Element): void {
el.textContent = "safe text";
}
function safeTypedParam(x: number): number {
return x + 1;
}
function safeUnknownHandling(x: unknown): string {
if (typeof x === "string") {
return x;
}
return String(x);
}

View file

@ -0,0 +1,56 @@
// Positive fixture: each snippet should trigger the named pattern.
// ts.code_exec.eval
function triggerEval(code: string): void {
eval(code);
}
// ts.code_exec.new_function
function triggerNewFunction(body: string): void {
const fn = new Function(body);
}
// ts.code_exec.settimeout_string
function triggerSetTimeout(): void {
setTimeout("alert(1)", 1000);
}
// ts.xss.document_write
function triggerDocumentWrite(data: string): void {
document.write(data);
}
// ts.xss.outer_html
function triggerOuterHtml(el: Element, data: string): void {
el.outerHTML = data;
}
// ts.xss.insert_adjacent_html
function triggerInsertAdjacentHtml(el: Element, data: string): void {
el.insertAdjacentHTML("beforeend", data);
}
// ts.quality.any_annotation
function triggerAnyAnnotation(x: any): void {
console.log(x);
}
// ts.quality.as_any
function triggerAsAny(x: unknown): void {
const y = x as any;
}
// ts.prototype.proto_assignment
function triggerProtoAssignment(obj: Record<string, unknown>): void {
obj.__proto__ = { malicious: true };
}
// ts.xss.location_assign
function triggerLocationAssign(url: string): void {
window.location = url;
}
// ts.xss.cookie_write
function triggerCookieWrite(sid: string): void {
document.cookie = "session=" + sid;
}

View file

@ -0,0 +1,20 @@
#include <stdlib.h>
void double_free_bug(int flag) {
char *buf = malloc(256);
if (flag) {
free(buf);
}
free(buf); // double free if flag was true
}
void conditional_free_safe(int flag) {
char *buf = malloc(256);
if (flag) {
free(buf);
buf = NULL;
}
if (buf != NULL) {
free(buf);
}
}

View file

@ -0,0 +1,24 @@
{
"description": "Double free when flag is true: free called twice on same pointer. Safe version nulls pointer after free.",
"tags": [
"cfg",
"state",
"double-free"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-double-close",
"severity": null,
"must_match": false,
"line_range": [
6,
10
],
"evidence_contains": [],
"notes": "Conditional double free \u2014 if flag is true, buf freed at line 6 and again at line 8. Aspirational state analysis finding."
}
]
}

View file

@ -0,0 +1,21 @@
#include <stdlib.h>
#include <string.h>
char *duplicate_string(const char *input) {
char *buf = malloc(strlen(input) + 1);
if (buf == NULL) return NULL;
strcpy(buf, input);
return buf;
}
void process_data(const char *input) {
char *copy = malloc(strlen(input) + 1);
if (copy == NULL) return;
strcpy(copy, input);
if (strlen(copy) > 100) {
return; // memory leak!
}
free(copy);
}

View file

@ -0,0 +1,45 @@
{
"description": "malloc leak on early return: process_data returns without free when strlen > 100",
"tags": [
"cfg",
"resource-leak"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "c.memory.strcpy",
"severity": null,
"must_match": true,
"line_range": [
5,
9
],
"evidence_contains": [],
"notes": "strcpy in duplicate_string \u2014 AST pattern match"
},
{
"rule_id": "c.memory.strcpy",
"severity": null,
"must_match": true,
"line_range": [
12,
16
],
"evidence_contains": [],
"notes": "strcpy in process_data \u2014 AST pattern match"
},
{
"rule_id": "cfg-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
15,
19
],
"evidence_contains": [],
"notes": "malloc at line 12 not freed before return at line 17 \u2014 aspirational CFG finding"
}
]
}

View file

@ -0,0 +1,29 @@
#include <stdio.h>
#include <stdlib.h>
int process_file(const char *path) {
FILE *f = fopen(path, "r");
if (f == NULL) return -1;
char buf[256];
if (fgets(buf, sizeof(buf), f) == NULL) {
return -2; // f leaked!
}
fclose(f);
return 0;
}
int process_file_safe(const char *path) {
FILE *f = fopen(path, "r");
if (f == NULL) return -1;
char buf[256];
if (fgets(buf, sizeof(buf), f) == NULL) {
fclose(f);
return -2;
}
fclose(f);
return 0;
}

View file

@ -0,0 +1,23 @@
{
"description": "File handle leak on early return in error branch vs safe version that closes before returning",
"tags": [
"cfg",
"resource-leak"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cfg-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
8,
12
],
"evidence_contains": [],
"notes": "fopen at line 5 not closed before return -2 at line 10 \u2014 aspirational CFG finding"
}
]
}

View file

@ -0,0 +1,20 @@
#include <stdio.h>
#include <stdlib.h>
void handle_command(int cmd, char *arg) {
switch (cmd) {
case 1:
system(arg);
break;
case 2:
printf("%s\n", arg);
break;
case 3:
system(arg); // no break - falls through
case 4:
printf("Done\n");
break;
default:
break;
}
}

View file

@ -0,0 +1,34 @@
{
"description": "system() calls in switch statement \u2014 case 3 falls through without break",
"tags": [
"cmdi",
"cfg"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "c.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
5,
9
],
"evidence_contains": [],
"notes": "system(arg) in case 1"
},
{
"rule_id": "c.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
11,
15
],
"evidence_contains": [],
"notes": "system(arg) in case 3 \u2014 falls through to case 4"
}
]
}

View file

@ -0,0 +1,12 @@
#include <stdio.h>
#include <stdlib.h>
void dangerous_pipe(char *user_input) {
char cmd[256];
sprintf(cmd, "cat %s", user_input);
FILE *fp = popen(cmd, "r");
char buf[1024];
fgets(buf, sizeof(buf), fp);
printf("%s", buf);
// pclose missing + command injection
}

View file

@ -0,0 +1,46 @@
{
"description": "Command injection via popen with sprintf, plus missing pclose resource leak",
"tags": [
"cmdi",
"state",
"mixed"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "c.cmdi.popen",
"severity": null,
"must_match": true,
"line_range": [
5,
9
],
"evidence_contains": [],
"notes": "popen executes shell command built from user input"
},
{
"rule_id": "c.memory.sprintf",
"severity": null,
"must_match": true,
"line_range": [
4,
8
],
"evidence_contains": [],
"notes": "sprintf used to build command \u2014 buffer overflow risk"
},
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
5,
13
],
"evidence_contains": [],
"notes": "popen at line 7 never pclose'd \u2014 aspirational state finding"
}
]
}

View file

@ -0,0 +1,11 @@
#include <stdio.h>
#include <stdlib.h>
void process_env() {
char *path = getenv("USER_PATH");
FILE *f = fopen(path, "r");
char buf[1024];
fgets(buf, sizeof(buf), f);
printf("%s", buf);
// Both: taint (getenv -> fopen) and resource leak (f not closed)
}

View file

@ -0,0 +1,35 @@
{
"description": "Combined taint and resource leak: getenv flows to fopen path, and file handle is never closed",
"tags": [
"taint",
"state",
"mixed"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
3,
8
],
"evidence_contains": [],
"notes": "getenv(\"USER_PATH\") flows into fopen as file path \u2014 path traversal"
},
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
4,
12
],
"evidence_contains": [],
"notes": "fopen at line 6 never closed \u2014 aspirational state finding"
}
]
}

View file

@ -0,0 +1,23 @@
#include <stdio.h>
void branch_leak(const char *path, int flag) {
FILE *f = fopen(path, "r");
if (flag) {
char buf[256];
fgets(buf, sizeof(buf), f);
fclose(f);
} else {
// f leaked in else
}
}
void both_close(const char *path, int flag) {
FILE *f = fopen(path, "r");
if (flag) {
char buf[256];
fgets(buf, sizeof(buf), f);
fclose(f);
} else {
fclose(f);
}
}

View file

@ -0,0 +1,24 @@
{
"description": "Branching resource lifecycle: one branch closes file, other leaks. Safe version closes in both branches.",
"tags": [
"state",
"resource-leak",
"branching"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-resource-leak-possible",
"severity": null,
"must_match": true,
"line_range": [
2,
13
],
"evidence_contains": [],
"notes": "fopen at line 4 only closed in if-branch, leaked in else-branch"
}
]
}

View file

@ -0,0 +1,27 @@
#include <stdio.h>
void read_leak(const char *path) {
FILE *f = fopen(path, "r");
char buf[256];
fgets(buf, sizeof(buf), f);
}
void read_close(const char *path) {
FILE *f = fopen(path, "r");
char buf[256];
fgets(buf, sizeof(buf), f);
fclose(f);
}
void double_close(const char *path) {
FILE *f = fopen(path, "r");
fclose(f);
fclose(f);
}
void use_after_close(const char *path) {
FILE *f = fopen(path, "r");
fclose(f);
char buf[256];
fgets(buf, sizeof(buf), f);
}

View file

@ -0,0 +1,45 @@
{
"description": "FILE* lifecycle: leak (no fclose), double close, and use after close patterns",
"tags": [
"state",
"resource-lifecycle"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": true,
"line_range": [
2,
8
],
"evidence_contains": [],
"notes": "fopen at line 4 never closed in read_leak \u2014 file handle leaked"
},
{
"rule_id": "state-double-close",
"severity": null,
"must_match": true,
"line_range": [
16,
21
],
"evidence_contains": [],
"notes": "fclose called twice on same FILE* in double_close"
},
{
"rule_id": "state-use-after-close",
"severity": null,
"must_match": true,
"line_range": [
23,
29
],
"evidence_contains": [],
"notes": "fgets called on f after fclose in use_after_close"
}
]
}

View file

@ -0,0 +1,21 @@
#include <stdio.h>
void loop_leak() {
int i;
for (i = 0; i < 10; i++) {
FILE *f = fopen("/tmp/test", "r");
char buf[256];
fgets(buf, sizeof(buf), f);
// f leaked each iteration!
}
}
void loop_close() {
int i;
for (i = 0; i < 10; i++) {
FILE *f = fopen("/tmp/test", "r");
char buf[256];
fgets(buf, sizeof(buf), f);
fclose(f);
}
}

View file

@ -0,0 +1,24 @@
{
"description": "Resource leak in loop: file opened each iteration but never closed. Safe version closes each iteration.",
"tags": [
"state",
"resource-leak",
"loop"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
4,
11
],
"evidence_contains": [],
"notes": "fopen inside loop body without fclose \u2014 leaked each iteration. Aspirational: loop-scoped leak detection."
}
]
}

View file

@ -0,0 +1,25 @@
#include <stdlib.h>
#include <string.h>
void alloc_leak() {
char *buf = malloc(1024);
strcpy(buf, "hello");
}
void alloc_free() {
char *buf = malloc(1024);
strcpy(buf, "hello");
free(buf);
}
void double_free() {
char *buf = malloc(1024);
free(buf);
free(buf);
}
void use_after_free() {
char *buf = malloc(1024);
free(buf);
strcpy(buf, "oops");
}

View file

@ -0,0 +1,45 @@
{
"description": "malloc lifecycle: leak (no free), double free, and use after free patterns",
"tags": [
"state",
"resource-lifecycle"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": true,
"line_range": [
3,
8
],
"evidence_contains": [],
"notes": "malloc at line 5 never freed in alloc_leak"
},
{
"rule_id": "state-double-close",
"severity": null,
"must_match": true,
"line_range": [
15,
20
],
"evidence_contains": [],
"notes": "free called twice on same pointer in double_free"
},
{
"rule_id": "state-use-after-close",
"severity": null,
"must_match": true,
"line_range": [
21,
26
],
"evidence_contains": [],
"notes": "strcpy on buf after free in use_after_free"
}
]
}

View file

@ -0,0 +1,28 @@
#include <stdio.h>
#include <string.h>
void copy_unsafe(char *input) {
char buf[64];
strcpy(buf, input);
printf("%s\n", buf);
}
void copy_safe(char *input) {
char buf[64];
strncpy(buf, input, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';
printf("%s\n", buf);
}
void gets_vuln() {
char buf[128];
gets(buf);
printf("%s\n", buf);
}
void concat_vuln(char *src1, char *src2) {
char buf[64];
strcpy(buf, src1);
strcat(buf, src2);
printf("%s\n", buf);
}

View file

@ -0,0 +1,56 @@
{
"description": "Classic C buffer overflow patterns: strcpy, gets, strcat without bounds checking",
"tags": [
"mem",
"buffer-overflow"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "c.memory.strcpy",
"severity": null,
"must_match": true,
"line_range": [
4,
8
],
"evidence_contains": [],
"notes": "strcpy without bounds check \u2014 potential buffer overflow"
},
{
"rule_id": "c.memory.gets",
"severity": null,
"must_match": true,
"line_range": [
17,
21
],
"evidence_contains": [],
"notes": "gets() is always unsafe \u2014 no way to limit input length"
},
{
"rule_id": "c.memory.strcpy",
"severity": null,
"must_match": true,
"line_range": [
23,
27
],
"evidence_contains": [],
"notes": "strcpy in concat_vuln \u2014 first unbounded copy"
},
{
"rule_id": "c.memory.strcat",
"severity": null,
"must_match": true,
"line_range": [
24,
28
],
"evidence_contains": [],
"notes": "strcat without bounds check \u2014 appends without size limit"
}
]
}

View file

@ -0,0 +1,16 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void run_from_env() {
char *cmd = getenv("USER_CMD");
system(cmd);
}
void run_safe() {
char *cmd = getenv("USER_CMD");
if (cmd == NULL) return;
if (strcmp(cmd, "ls") == 0 || strcmp(cmd, "date") == 0) {
system(cmd);
}
}

View file

@ -0,0 +1,56 @@
{
"description": "getenv flows directly to system() call \u2014 classic command injection. Safe version uses allowlist comparison.",
"tags": [
"taint",
"cmdi"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "c.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
5,
9
],
"evidence_contains": [],
"notes": "AST pattern detects system() call"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
4,
9
],
"evidence_contains": [],
"notes": "getenv(\"USER_CMD\") flows directly into system(cmd) without sanitization"
},
{
"rule_id": "c.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
12,
16
],
"evidence_contains": [],
"notes": "AST pattern still fires on system() in safe version \u2014 pattern is syntactic"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": false,
"line_range": [
9,
16
],
"evidence_contains": [],
"notes": "Safe version with strcmp allowlist \u2014 ideally suppressed but scanner may not model strcmp as validation"
}
]
}

View file

@ -0,0 +1,14 @@
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
void execute_cmd(char *user_input) {
char cmd[256];
sprintf(cmd, "grep -r '%s' /var/log/", user_input);
FILE *fp = popen(cmd, "r");
char buf[1024];
while (fgets(buf, sizeof(buf), fp)) {
printf("%s", buf);
}
pclose(fp);
}

View file

@ -0,0 +1,45 @@
{
"description": "Command injection via popen: user input interpolated into shell command via sprintf then passed to popen",
"tags": [
"taint",
"cmdi"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "c.cmdi.popen",
"severity": null,
"must_match": true,
"line_range": [
6,
10
],
"evidence_contains": [],
"notes": "popen() executes shell command constructed from user input"
},
{
"rule_id": "c.memory.sprintf",
"severity": null,
"must_match": true,
"line_range": [
5,
9
],
"evidence_contains": [],
"notes": "sprintf used to build command string \u2014 buffer overflow risk"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": false,
"line_range": [
3,
10
],
"evidence_contains": [],
"notes": "user_input flows through sprintf into popen \u2014 taint engine may not track through sprintf buffer"
}
]
}

View file

@ -0,0 +1,9 @@
#include <stdio.h>
#include <stdlib.h>
void write_config() {
char *path = getenv("CONFIG_PATH");
FILE *f = fopen(path, "w");
fprintf(f, "config data\n");
fclose(f);
}

View file

@ -0,0 +1,23 @@
{
"description": "Environment variable flows to fopen path \u2014 path traversal / arbitrary file write",
"tags": [
"taint",
"path-traversal"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
3,
8
],
"evidence_contains": [],
"notes": "getenv(\"CONFIG_PATH\") flows directly into fopen as file path \u2014 arbitrary file write"
}
]
}

View file

@ -0,0 +1,14 @@
#include <stdio.h>
#include <stdlib.h>
void print_user_input(char *input) {
printf(input);
}
void print_safe(char *input) {
printf("%s", input);
}
void sprintf_vuln(char *buf, char *user_input) {
sprintf(buf, user_input);
}

View file

@ -0,0 +1,34 @@
{
"description": "Format string vulnerabilities: printf with user-controlled format and sprintf with user-controlled format",
"tags": [
"taint",
"fmt"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "c.memory.printf_no_fmt",
"severity": null,
"must_match": true,
"line_range": [
3,
7
],
"evidence_contains": [],
"notes": "printf(input) \u2014 user-controlled format string"
},
{
"rule_id": "c.memory.sprintf",
"severity": null,
"must_match": true,
"line_range": [
11,
15
],
"evidence_contains": [],
"notes": "sprintf with user-controlled format string \u2014 both format vuln and buffer overflow risk"
}
]
}

View file

@ -0,0 +1,13 @@
#include <stdio.h>
void read_input() {
char name[32];
scanf("%s", name);
printf("Hello, %s\n", name);
}
void read_safe() {
char name[32];
scanf("%31s", name);
printf("Hello, %s\n", name);
}

View file

@ -0,0 +1,34 @@
{
"description": "scanf with unbounded %s format specifier vs safe %31s with width limit",
"tags": [
"mem",
"scanf"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "c.memory.scanf_percent_s",
"severity": null,
"must_match": true,
"line_range": [
3,
7
],
"evidence_contains": [],
"notes": "scanf(\"%s\", name) \u2014 unbounded read into stack buffer"
},
{
"rule_id": "c.memory.scanf_percent_s",
"severity": null,
"must_match": false,
"line_range": [
9,
13
],
"evidence_contains": [],
"notes": "scanf(\"%31s\", name) \u2014 bounded read, ideally no finding but AST pattern may still fire"
}
]
}

View file

@ -0,0 +1,10 @@
#include <cstdlib>
#include <functional>
#include <string>
std::function<void()> create_dangerous_lambda(const char *user_input) {
std::string cmd = std::string("echo ") + user_input;
return [cmd]() {
system(cmd.c_str());
};
}

View file

@ -0,0 +1,24 @@
{
"description": "system() call inside lambda capturing user input by value",
"tags": [
"cmdi",
"cfg",
"lambda"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cpp.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
6,
10
],
"evidence_contains": [],
"notes": "system() called inside lambda with captured user-derived command string"
}
]
}

View file

@ -0,0 +1,19 @@
#include <cstdlib>
#include <cstdio>
namespace security {
void validate(const char *input) {
if (input == nullptr) return;
}
}
namespace execution {
void run(const char *cmd) {
system(cmd);
}
}
void handler(const char *user_input) {
security::validate(user_input);
execution::run(user_input);
}

View file

@ -0,0 +1,35 @@
{
"description": "system() in namespace \u2014 validation function does not actually sanitize input",
"tags": [
"cmdi",
"cfg",
"namespace"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cpp.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
10,
14
],
"evidence_contains": [],
"notes": "system(cmd) in execution::run \u2014 AST pattern detects system() call"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": false,
"line_range": [
15,
20
],
"evidence_contains": [],
"notes": "user_input flows through validate (which only null-checks) to system \u2014 aspirational cross-function taint"
}
]
}

View file

@ -0,0 +1,19 @@
#include <fstream>
#include <cstdio>
#include <string>
std::string read_raii(const char *path) {
std::ifstream file(path);
std::string content;
std::getline(file, content);
return content;
// RAII: ifstream destructor closes
}
std::string read_manual(const char *path) {
FILE *f = fopen(path, "r");
char buf[256];
fgets(buf, sizeof(buf), f);
// f not closed -- manual leak
return std::string(buf);
}

View file

@ -0,0 +1,24 @@
{
"description": "RAII ifstream vs manual FILE* \u2014 RAII auto-closes, manual version leaks",
"tags": [
"cfg",
"resource-leak",
"raii"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cfg-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
12,
20
],
"evidence_contains": [],
"notes": "fopen at line 14 never fclose'd in read_manual \u2014 aspirational CFG finding"
}
]
}

View file

@ -0,0 +1,30 @@
#include <cstdio>
#include <stdexcept>
void process_file(const char *path) {
FILE *f = fopen(path, "r");
try {
char buf[256];
if (fgets(buf, sizeof(buf), f) == NULL) {
throw std::runtime_error("read failed");
}
fclose(f);
} catch (...) {
// f leaked in catch
throw;
}
}
void process_safe(const char *path) {
FILE *f = fopen(path, "r");
try {
char buf[256];
if (fgets(buf, sizeof(buf), f) == NULL) {
fclose(f);
throw std::runtime_error("read failed");
}
fclose(f);
} catch (...) {
throw;
}
}

View file

@ -0,0 +1,24 @@
{
"description": "Resource leak in exception path: fopen not closed when exception thrown. Safe version closes before throw.",
"tags": [
"cfg",
"resource-leak",
"exception"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cfg-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
3,
17
],
"evidence_contains": [],
"notes": "fopen at line 5 leaked when exception thrown at line 9 \u2014 catch block re-throws without closing. Aspirational."
}
]
}

View file

@ -0,0 +1,10 @@
#include <cstdio>
#include <cstdlib>
#include <string>
void dangerous(const char *user_input) {
char cmd[256];
sprintf(cmd, "cat %s", user_input);
system(cmd);
printf(user_input); // also format string vuln
}

View file

@ -0,0 +1,35 @@
{
"description": "Multiple vulnerabilities: command injection via system() and format string via printf(user_input)",
"tags": [
"cmdi",
"fmt",
"mixed"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cpp.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
6,
10
],
"evidence_contains": [],
"notes": "system(cmd) where cmd built from user input via sprintf"
},
{
"rule_id": "cpp.memory.printf_no_fmt",
"severity": null,
"must_match": true,
"line_range": [
7,
11
],
"evidence_contains": [],
"notes": "printf(user_input) \u2014 user-controlled format string"
}
]
}

View file

@ -0,0 +1,10 @@
#include <cstdlib>
#include <cstdio>
void env_leak() {
const char *path = std::getenv("USER_PATH");
FILE *f = fopen(path, "r");
char buf[1024];
fgets(buf, sizeof(buf), f);
// taint (getenv -> fopen) + resource leak
}

View file

@ -0,0 +1,35 @@
{
"description": "Combined taint and resource leak: std::getenv flows to fopen, file handle never closed",
"tags": [
"taint",
"state",
"mixed"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
3,
8
],
"evidence_contains": [],
"notes": "std::getenv(\"USER_PATH\") flows to fopen as file path \u2014 path traversal"
},
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
4,
11
],
"evidence_contains": [],
"notes": "fopen at line 6 never closed \u2014 aspirational state finding"
}
]
}

View file

@ -0,0 +1,27 @@
#include <cstdio>
void leak() {
FILE *f = fopen("/tmp/test", "r");
char buf[256];
fgets(buf, sizeof(buf), f);
}
void clean() {
FILE *f = fopen("/tmp/test", "r");
char buf[256];
fgets(buf, sizeof(buf), f);
fclose(f);
}
void double_close() {
FILE *f = fopen("/tmp/test", "r");
fclose(f);
fclose(f);
}
void use_after_close() {
FILE *f = fopen("/tmp/test", "r");
fclose(f);
char buf[256];
fgets(buf, sizeof(buf), f);
}

View file

@ -0,0 +1,45 @@
{
"description": "C++ FILE* lifecycle patterns: leak, double close, use after close",
"tags": [
"state",
"resource-lifecycle"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": true,
"line_range": [
2,
8
],
"evidence_contains": [],
"notes": "fopen at line 4 never closed in leak()"
},
{
"rule_id": "state-double-close",
"severity": null,
"must_match": true,
"line_range": [
16,
21
],
"evidence_contains": [],
"notes": "fclose called twice on same FILE* in double_close()"
},
{
"rule_id": "state-use-after-close",
"severity": null,
"must_match": true,
"line_range": [
23,
29
],
"evidence_contains": [],
"notes": "fgets on f after fclose in use_after_close()"
}
]
}

View file

@ -0,0 +1,11 @@
#include <cstdlib>
#include <cstring>
void branch_leak(int flag) {
char *buf = (char*)malloc(256);
if (flag) {
strcpy(buf, "hello");
free(buf);
}
// buf leaked if !flag
}

View file

@ -0,0 +1,24 @@
{
"description": "C++ malloc branch leak: only freed in one branch of conditional",
"tags": [
"state",
"resource-leak",
"branching"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-resource-leak-possible",
"severity": null,
"must_match": false,
"line_range": [
3,
12
],
"evidence_contains": [],
"notes": "malloc at line 5 only freed when flag is true \u2014 aspirational branch-aware state analysis"
}
]
}

View file

@ -0,0 +1,18 @@
#include <cstring>
void leak() {
char *buf = new char[1024];
strcpy(buf, "hello");
}
void clean() {
char *buf = new char[1024];
strcpy(buf, "hello");
delete[] buf;
}
void double_delete() {
char *buf = new char[1024];
delete[] buf;
delete[] buf;
}

View file

@ -0,0 +1,34 @@
{
"description": "C++ new[]/delete[] lifecycle: leak and double delete patterns",
"tags": [
"state",
"resource-lifecycle"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
2,
7
],
"evidence_contains": [],
"notes": "new char[1024] at line 4 never deleted \u2014 aspirational, requires new/delete tracking"
},
{
"rule_id": "state-double-close",
"severity": null,
"must_match": false,
"line_range": [
14,
19
],
"evidence_contains": [],
"notes": "delete[] called twice \u2014 aspirational, requires new/delete tracking"
}
]
}

View file

@ -0,0 +1,12 @@
#include <memory>
#include <cstdlib>
void smart_clean() {
auto ptr = std::make_unique<int>(42);
// automatically cleaned up
}
void raw_leak() {
int *ptr = new int(42);
// never deleted
}

View file

@ -0,0 +1,24 @@
{
"description": "Smart pointer vs raw new: unique_ptr auto-cleans, raw pointer leaks",
"tags": [
"state",
"resource-leak",
"smart-pointer"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
8,
13
],
"evidence_contains": [],
"notes": "new int(42) at line 10 never deleted \u2014 aspirational, requires new/delete tracking"
}
]
}

View file

@ -0,0 +1,16 @@
#include <cstdlib>
#include <string>
void execute_user_cmd() {
const char *cmd = std::getenv("USER_CMD");
system(cmd);
}
void execute_safe() {
const char *cmd = std::getenv("USER_CMD");
if (cmd == nullptr) return;
std::string s(cmd);
if (s == "ls" || s == "date") {
system(cmd);
}
}

View file

@ -0,0 +1,45 @@
{
"description": "C++ command injection: std::getenv flows to system(). Safe version uses allowlist.",
"tags": [
"taint",
"cmdi"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cpp.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
4,
8
],
"evidence_contains": [],
"notes": "system(cmd) where cmd comes from std::getenv"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
3,
8
],
"evidence_contains": [],
"notes": "std::getenv flows directly to system() without sanitization"
},
{
"rule_id": "cpp.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
12,
16
],
"evidence_contains": [],
"notes": "AST pattern still matches system() in safe version"
}
]
}

View file

@ -0,0 +1,9 @@
#include <cstdlib>
#include <string>
int main() {
char *home = std::getenv("HOME");
std::string cmd = "ls " + std::string(home);
system(cmd.c_str());
return 0;
}

View file

@ -0,0 +1,34 @@
{
"description": "Environment variable concatenated into system() call \u2014 command injection via HOME",
"tags": [
"taint",
"cmdi"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cpp.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
5,
9
],
"evidence_contains": [],
"notes": "system() called with command built from std::getenv(\"HOME\")"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
3,
9
],
"evidence_contains": [],
"notes": "std::getenv flows through string concatenation into system()"
}
]
}

View file

@ -0,0 +1,10 @@
#include <cstdio>
#include <cstdlib>
void print_unsafe(const char *user_input) {
printf(user_input);
}
void print_safe(const char *user_input) {
printf("%s", user_input);
}

View file

@ -0,0 +1,23 @@
{
"description": "C++ format string vulnerability: printf with user-controlled format argument",
"tags": [
"taint",
"fmt"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cpp.memory.printf_no_fmt",
"severity": null,
"must_match": true,
"line_range": [
3,
7
],
"evidence_contains": [],
"notes": "printf(user_input) \u2014 user-controlled format string"
}
]
}

View file

@ -0,0 +1,12 @@
#include <cstdio>
#include <cstring>
void copy_unsafe(const char *input) {
char buf[64];
strcpy(buf, input);
}
void gets_input() {
char buf[128];
gets(buf);
}

View file

@ -0,0 +1,34 @@
{
"description": "C++ legacy C function usage: strcpy and gets without bounds checking",
"tags": [
"mem",
"buffer-overflow"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cpp.memory.strcpy",
"severity": null,
"must_match": true,
"line_range": [
4,
8
],
"evidence_contains": [],
"notes": "strcpy without bounds check in C++ code"
},
{
"rule_id": "cpp.memory.gets",
"severity": null,
"must_match": true,
"line_range": [
9,
13
],
"evidence_contains": [],
"notes": "gets() always unsafe \u2014 no bounds checking"
}
]
}

View file

@ -0,0 +1,13 @@
#include <cstdio>
#include <cstdlib>
#include <string>
void run_command(const std::string &user_input) {
std::string cmd = "grep " + user_input + " /var/log/syslog";
FILE *fp = popen(cmd.c_str(), "r");
char buf[1024];
while (fgets(buf, sizeof(buf), fp)) {
printf("%s", buf);
}
pclose(fp);
}

View file

@ -0,0 +1,23 @@
{
"description": "Command injection via popen: user input concatenated into shell command string",
"tags": [
"taint",
"cmdi"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cpp.cmdi.popen",
"severity": null,
"must_match": true,
"line_range": [
5,
9
],
"evidence_contains": [],
"notes": "popen executes command string built from user input via string concatenation"
}
]
}

View file

@ -0,0 +1,12 @@
#include <cstring>
#include <cstdio>
struct Header {
int type;
int length;
};
void parse_packet(const char *data) {
Header *hdr = reinterpret_cast<Header*>(const_cast<char*>(data));
printf("Type: %d, Length: %d\n", hdr->type, hdr->length);
}

View file

@ -0,0 +1,34 @@
{
"description": "Dangerous C++ casts: reinterpret_cast and const_cast used to parse raw data",
"tags": [
"cast",
"unsafe"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cpp.memory.reinterpret_cast",
"severity": null,
"must_match": true,
"line_range": [
8,
12
],
"evidence_contains": [],
"notes": "reinterpret_cast<Header*> \u2014 type punning raw bytes to struct pointer"
},
{
"rule_id": "cpp.memory.const_cast",
"severity": null,
"must_match": true,
"line_range": [
8,
12
],
"evidence_contains": [],
"notes": "const_cast<char*> removes const qualifier from data pointer"
}
]
}

View file

@ -0,0 +1,23 @@
{
"description": "Idiomatic Go defer close vs manual close with early-return leak path",
"tags": [
"cfg",
"resource-leak"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cfg-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
21,
35
],
"evidence_contains": [],
"notes": "readLeaky returns on line 32 error path without closing f \u2014 missing defer f.Close()"
}
]
}

View file

@ -0,0 +1,36 @@
package main
import (
"os"
)
func readSafe(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
buf := make([]byte, 1024)
n, err := f.Read(buf)
if err != nil {
return nil, err
}
return buf[:n], nil
}
func readLeaky(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
// Missing defer f.Close()
buf := make([]byte, 1024)
n, err := f.Read(buf)
if err != nil {
return nil, err // f leaked
}
f.Close()
return buf[:n], nil
}

View file

@ -0,0 +1,47 @@
{
"description": "Error fallthrough in handleRequest allows exec with potentially zero-value command; safe version returns on error",
"tags": [
"cfg",
"cmdi",
"error-handling"
],
"modes": [
"full",
"ast"
],
"expected": [
{
"rule_id": "go.cmdi.exec_command",
"severity": null,
"must_match": true,
"line_range": [
19,
23
],
"evidence_contains": [],
"notes": "AST pattern detects exec.Command in handleRequest"
},
{
"rule_id": "go.cmdi.exec_command",
"severity": null,
"must_match": true,
"line_range": [
34,
38
],
"evidence_contains": [],
"notes": "AST pattern detects exec.Command in handleRequestSafe"
},
{
"rule_id": "cfg-error-fallthrough",
"severity": null,
"must_match": false,
"line_range": [
14,
23
],
"evidence_contains": [],
"notes": "Error from json.Decode not returned \u2014 execution falls through to exec.Command"
}
]
}

View file

@ -0,0 +1,38 @@
package main
import (
"encoding/json"
"fmt"
"net/http"
"os/exec"
)
func handleRequest(w http.ResponseWriter, r *http.Request) {
var req struct {
Command string `json:"command"`
}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
fmt.Println("bad request")
// falls through!
}
cmd := exec.Command("sh", "-c", req.Command)
cmd.Run()
}
func handleRequestSafe(w http.ResponseWriter, r *http.Request) {
var req struct {
Command string `json:"command"`
}
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, "Bad request", 400)
return
}
cmd := exec.Command("sh", "-c", req.Command)
cmd.Run()
}

View file

@ -0,0 +1,24 @@
{
"description": "Resource leak on panic path in riskyOperation; safeOperation uses defer; recoverWrapper catches panics",
"tags": [
"cfg",
"resource-leak",
"panic"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cfg-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
7,
18
],
"evidence_contains": [],
"notes": "If code between os.Open and f.Close panics, f is leaked \u2014 no defer f.Close()"
}
]
}

View file

@ -0,0 +1,36 @@
package main
import (
"fmt"
"os"
)
func riskyOperation() {
f, err := os.Open("/tmp/test")
if err != nil {
panic(err)
}
// f leaked on panic path
buf := make([]byte, 1024)
f.Read(buf)
f.Close()
}
func safeOperation() {
f, err := os.Open("/tmp/test")
if err != nil {
panic(err)
}
defer f.Close()
buf := make([]byte, 1024)
f.Read(buf)
}
func recoverWrapper() {
defer func() {
if r := recover(); r != nil {
fmt.Println("Recovered:", r)
}
}()
riskyOperation()
}

View file

@ -0,0 +1,36 @@
{
"description": "Goroutine with select-based timeout running user-controlled command via exec.Command",
"tags": [
"cfg",
"cmdi",
"concurrency"
],
"modes": [
"full",
"ast"
],
"expected": [
{
"rule_id": "go.cmdi.exec_command",
"severity": null,
"must_match": true,
"line_range": [
9,
13
],
"evidence_contains": [],
"notes": "AST pattern detects exec.Command in runWithTimeout"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": false,
"line_range": [
27,
32
],
"evidence_contains": [],
"notes": "os.Getenv(\"CMD\") flows through runWithTimeout to exec.Command \u2014 cross-function taint may not resolve"
}
]
}

View file

@ -0,0 +1,32 @@
package main
import (
"fmt"
"os"
"os/exec"
"time"
)
func runWithTimeout(command string, timeout time.Duration) (string, error) {
cmd := exec.Command("sh", "-c", command)
done := make(chan string)
go func() {
output, _ := cmd.Output()
done <- string(output)
}()
select {
case result := <-done:
return result, nil
case <-time.After(timeout):
cmd.Process.Kill()
return "", fmt.Errorf("timeout")
}
}
func main() {
cmd := os.Getenv("CMD")
result, _ := runWithTimeout(cmd, 5*time.Second)
fmt.Println(result)
}

View file

@ -0,0 +1,72 @@
{
"description": "HTTP handlers with command injection, path traversal, missing auth, and resource leak",
"tags": [
"taint",
"state",
"cfg",
"cmdi",
"path-traversal",
"auth-gap",
"mixed"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "go.cmdi.exec_command",
"severity": null,
"must_match": true,
"line_range": [
11,
15
],
"evidence_contains": [],
"notes": "AST pattern detects exec.Command in adminHandler"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
9,
15
],
"evidence_contains": [],
"notes": "r.URL.Query().Get(\"cmd\") flows directly into exec.Command(\"sh\", \"-c\", cmd)"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
16,
21
],
"evidence_contains": [],
"notes": "r.URL.Query().Get(\"path\") flows into os.Open \u2014 path traversal"
},
{
"rule_id": "cfg-auth-gap",
"severity": null,
"must_match": false,
"line_range": [
8,
16
],
"evidence_contains": [],
"notes": "adminHandler executes commands without any authentication or authorization check"
},
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
17,
25
],
"evidence_contains": [],
"notes": "os.Open on line 19 \u2014 f never closed in readHandler"
}
]
}

Some files were not shown because too many files have changed in this diff Show more