mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-15 20:05:13 +02:00
Phase 1 (#33)
* 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:
parent
19b578c5c4
commit
1bbe4b1cfb
456 changed files with 25628 additions and 1228 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
12
tests/fixtures/c_utils/expectations.json
vendored
12
tests/fixtures/c_utils/expectations.json
vendored
|
|
@ -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": [],
|
||||
|
|
|
|||
8
tests/fixtures/express_app/expectations.json
vendored
8
tests/fixtures/express_app/expectations.json
vendored
|
|
@ -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": {
|
||||
|
|
|
|||
8
tests/fixtures/flask_app/expectations.json
vendored
8
tests/fixtures/flask_app/expectations.json
vendored
|
|
@ -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": {
|
||||
|
|
|
|||
2
tests/fixtures/go_server/expectations.json
vendored
2
tests/fixtures/go_server/expectations.json
vendored
|
|
@ -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": [],
|
||||
|
|
|
|||
10
tests/fixtures/java_service/expectations.json
vendored
10
tests/fixtures/java_service/expectations.json
vendored
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
24
tests/fixtures/patterns/c/negative.c
vendored
Normal 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
50
tests/fixtures/patterns/c/positive.c
vendored
Normal 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);
|
||||
}
|
||||
24
tests/fixtures/patterns/cpp/negative.cpp
vendored
Normal file
24
tests/fixtures/patterns/cpp/negative.cpp
vendored
Normal 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");
|
||||
}
|
||||
49
tests/fixtures/patterns/cpp/positive.cpp
vendored
Normal file
49
tests/fixtures/patterns/cpp/positive.cpp
vendored
Normal 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
23
tests/fixtures/patterns/go/negative.go
vendored
Normal 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
55
tests/fixtures/patterns/go/positive.go
vendored
Normal 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
|
||||
}
|
||||
22
tests/fixtures/patterns/java/negative.java
vendored
Normal file
22
tests/fixtures/patterns/java/negative.java
vendored
Normal 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");
|
||||
}
|
||||
}
|
||||
48
tests/fixtures/patterns/java/positive.java
vendored
Normal file
48
tests/fixtures/patterns/java/positive.java
vendored
Normal 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>");
|
||||
}
|
||||
}
|
||||
25
tests/fixtures/patterns/javascript/negative.js
vendored
Normal file
25
tests/fixtures/patterns/javascript/negative.js
vendored
Normal 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);
|
||||
}
|
||||
51
tests/fixtures/patterns/javascript/positive.js
vendored
Normal file
51
tests/fixtures/patterns/javascript/positive.js
vendored
Normal 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();
|
||||
}
|
||||
25
tests/fixtures/patterns/php/negative.php
vendored
Normal file
25
tests/fixtures/patterns/php/negative.php
vendored
Normal 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);
|
||||
}
|
||||
57
tests/fixtures/patterns/php/positive.php
vendored
Normal file
57
tests/fixtures/patterns/php/positive.php
vendored
Normal 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();
|
||||
}
|
||||
23
tests/fixtures/patterns/python/negative.py
vendored
Normal file
23
tests/fixtures/patterns/python/negative.py
vendored
Normal 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)
|
||||
51
tests/fixtures/patterns/python/positive.py
vendored
Normal file
51
tests/fixtures/patterns/python/positive.py
vendored
Normal 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)
|
||||
25
tests/fixtures/patterns/ruby/negative.rb
vendored
Normal file
25
tests/fixtures/patterns/ruby/negative.rb
vendored
Normal 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
|
||||
51
tests/fixtures/patterns/ruby/positive.rb
vendored
Normal file
51
tests/fixtures/patterns/ruby/positive.rb
vendored
Normal 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
|
||||
36
tests/fixtures/patterns/rust/negative.rs
vendored
Normal file
36
tests/fixtures/patterns/rust/negative.rs
vendored
Normal 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();
|
||||
}
|
||||
78
tests/fixtures/patterns/rust/positive.rs
vendored
Normal file
78
tests/fixtures/patterns/rust/positive.rs
vendored
Normal 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);
|
||||
}
|
||||
25
tests/fixtures/patterns/typescript/negative.ts
vendored
Normal file
25
tests/fixtures/patterns/typescript/negative.ts
vendored
Normal 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);
|
||||
}
|
||||
56
tests/fixtures/patterns/typescript/positive.ts
vendored
Normal file
56
tests/fixtures/patterns/typescript/positive.ts
vendored
Normal 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;
|
||||
}
|
||||
20
tests/fixtures/real_world/c/cfg/double_free.c
vendored
Normal file
20
tests/fixtures/real_world/c/cfg/double_free.c
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
24
tests/fixtures/real_world/c/cfg/double_free.expect.json
vendored
Normal file
24
tests/fixtures/real_world/c/cfg/double_free.expect.json
vendored
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
||||
21
tests/fixtures/real_world/c/cfg/malloc_branches.c
vendored
Normal file
21
tests/fixtures/real_world/c/cfg/malloc_branches.c
vendored
Normal 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);
|
||||
}
|
||||
45
tests/fixtures/real_world/c/cfg/malloc_branches.expect.json
vendored
Normal file
45
tests/fixtures/real_world/c/cfg/malloc_branches.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
29
tests/fixtures/real_world/c/cfg/resource_leak_branches.c
vendored
Normal file
29
tests/fixtures/real_world/c/cfg/resource_leak_branches.c
vendored
Normal 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;
|
||||
}
|
||||
23
tests/fixtures/real_world/c/cfg/resource_leak_branches.expect.json
vendored
Normal file
23
tests/fixtures/real_world/c/cfg/resource_leak_branches.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
20
tests/fixtures/real_world/c/cfg/switch_fallthrough.c
vendored
Normal file
20
tests/fixtures/real_world/c/cfg/switch_fallthrough.c
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
34
tests/fixtures/real_world/c/cfg/switch_fallthrough.expect.json
vendored
Normal file
34
tests/fixtures/real_world/c/cfg/switch_fallthrough.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
tests/fixtures/real_world/c/mixed/cmdi_and_leak.c
vendored
Normal file
12
tests/fixtures/real_world/c/mixed/cmdi_and_leak.c
vendored
Normal 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
|
||||
}
|
||||
46
tests/fixtures/real_world/c/mixed/cmdi_and_leak.expect.json
vendored
Normal file
46
tests/fixtures/real_world/c/mixed/cmdi_and_leak.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
tests/fixtures/real_world/c/mixed/taint_plus_leak.c
vendored
Normal file
11
tests/fixtures/real_world/c/mixed/taint_plus_leak.c
vendored
Normal 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)
|
||||
}
|
||||
35
tests/fixtures/real_world/c/mixed/taint_plus_leak.expect.json
vendored
Normal file
35
tests/fixtures/real_world/c/mixed/taint_plus_leak.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
tests/fixtures/real_world/c/state/branch_state.c
vendored
Normal file
23
tests/fixtures/real_world/c/state/branch_state.c
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
24
tests/fixtures/real_world/c/state/branch_state.expect.json
vendored
Normal file
24
tests/fixtures/real_world/c/state/branch_state.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
tests/fixtures/real_world/c/state/fopen_lifecycle.c
vendored
Normal file
27
tests/fixtures/real_world/c/state/fopen_lifecycle.c
vendored
Normal 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);
|
||||
}
|
||||
45
tests/fixtures/real_world/c/state/fopen_lifecycle.expect.json
vendored
Normal file
45
tests/fixtures/real_world/c/state/fopen_lifecycle.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
21
tests/fixtures/real_world/c/state/loop_state.c
vendored
Normal file
21
tests/fixtures/real_world/c/state/loop_state.c
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
24
tests/fixtures/real_world/c/state/loop_state.expect.json
vendored
Normal file
24
tests/fixtures/real_world/c/state/loop_state.expect.json
vendored
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
||||
25
tests/fixtures/real_world/c/state/malloc_lifecycle.c
vendored
Normal file
25
tests/fixtures/real_world/c/state/malloc_lifecycle.c
vendored
Normal 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");
|
||||
}
|
||||
45
tests/fixtures/real_world/c/state/malloc_lifecycle.expect.json
vendored
Normal file
45
tests/fixtures/real_world/c/state/malloc_lifecycle.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
28
tests/fixtures/real_world/c/taint/buffer_overflow.c
vendored
Normal file
28
tests/fixtures/real_world/c/taint/buffer_overflow.c
vendored
Normal 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);
|
||||
}
|
||||
56
tests/fixtures/real_world/c/taint/buffer_overflow.expect.json
vendored
Normal file
56
tests/fixtures/real_world/c/taint/buffer_overflow.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
tests/fixtures/real_world/c/taint/cmdi_getenv.c
vendored
Normal file
16
tests/fixtures/real_world/c/taint/cmdi_getenv.c
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
56
tests/fixtures/real_world/c/taint/cmdi_getenv.expect.json
vendored
Normal file
56
tests/fixtures/real_world/c/taint/cmdi_getenv.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
tests/fixtures/real_world/c/taint/cmdi_popen.c
vendored
Normal file
14
tests/fixtures/real_world/c/taint/cmdi_popen.c
vendored
Normal 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);
|
||||
}
|
||||
45
tests/fixtures/real_world/c/taint/cmdi_popen.expect.json
vendored
Normal file
45
tests/fixtures/real_world/c/taint/cmdi_popen.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
9
tests/fixtures/real_world/c/taint/env_to_file.c
vendored
Normal file
9
tests/fixtures/real_world/c/taint/env_to_file.c
vendored
Normal 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);
|
||||
}
|
||||
23
tests/fixtures/real_world/c/taint/env_to_file.expect.json
vendored
Normal file
23
tests/fixtures/real_world/c/taint/env_to_file.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
14
tests/fixtures/real_world/c/taint/format_string.c
vendored
Normal file
14
tests/fixtures/real_world/c/taint/format_string.c
vendored
Normal 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);
|
||||
}
|
||||
34
tests/fixtures/real_world/c/taint/format_string.expect.json
vendored
Normal file
34
tests/fixtures/real_world/c/taint/format_string.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
tests/fixtures/real_world/c/taint/scanf_overflow.c
vendored
Normal file
13
tests/fixtures/real_world/c/taint/scanf_overflow.c
vendored
Normal 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);
|
||||
}
|
||||
34
tests/fixtures/real_world/c/taint/scanf_overflow.expect.json
vendored
Normal file
34
tests/fixtures/real_world/c/taint/scanf_overflow.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
tests/fixtures/real_world/cpp/cfg/lambda_capture.cpp
vendored
Normal file
10
tests/fixtures/real_world/cpp/cfg/lambda_capture.cpp
vendored
Normal 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());
|
||||
};
|
||||
}
|
||||
24
tests/fixtures/real_world/cpp/cfg/lambda_capture.expect.json
vendored
Normal file
24
tests/fixtures/real_world/cpp/cfg/lambda_capture.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
tests/fixtures/real_world/cpp/cfg/namespace_scope.cpp
vendored
Normal file
19
tests/fixtures/real_world/cpp/cfg/namespace_scope.cpp
vendored
Normal 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);
|
||||
}
|
||||
35
tests/fixtures/real_world/cpp/cfg/namespace_scope.expect.json
vendored
Normal file
35
tests/fixtures/real_world/cpp/cfg/namespace_scope.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
19
tests/fixtures/real_world/cpp/cfg/raii_vs_manual.cpp
vendored
Normal file
19
tests/fixtures/real_world/cpp/cfg/raii_vs_manual.cpp
vendored
Normal 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);
|
||||
}
|
||||
24
tests/fixtures/real_world/cpp/cfg/raii_vs_manual.expect.json
vendored
Normal file
24
tests/fixtures/real_world/cpp/cfg/raii_vs_manual.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
30
tests/fixtures/real_world/cpp/cfg/try_catch.cpp
vendored
Normal file
30
tests/fixtures/real_world/cpp/cfg/try_catch.cpp
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
24
tests/fixtures/real_world/cpp/cfg/try_catch.expect.json
vendored
Normal file
24
tests/fixtures/real_world/cpp/cfg/try_catch.expect.json
vendored
Normal 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."
|
||||
}
|
||||
]
|
||||
}
|
||||
10
tests/fixtures/real_world/cpp/mixed/cmdi_format.cpp
vendored
Normal file
10
tests/fixtures/real_world/cpp/mixed/cmdi_format.cpp
vendored
Normal 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
|
||||
}
|
||||
35
tests/fixtures/real_world/cpp/mixed/cmdi_format.expect.json
vendored
Normal file
35
tests/fixtures/real_world/cpp/mixed/cmdi_format.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
tests/fixtures/real_world/cpp/mixed/taint_leak.cpp
vendored
Normal file
10
tests/fixtures/real_world/cpp/mixed/taint_leak.cpp
vendored
Normal 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
|
||||
}
|
||||
35
tests/fixtures/real_world/cpp/mixed/taint_leak.expect.json
vendored
Normal file
35
tests/fixtures/real_world/cpp/mixed/taint_leak.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
27
tests/fixtures/real_world/cpp/state/fopen_lifecycle.cpp
vendored
Normal file
27
tests/fixtures/real_world/cpp/state/fopen_lifecycle.cpp
vendored
Normal 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);
|
||||
}
|
||||
45
tests/fixtures/real_world/cpp/state/fopen_lifecycle.expect.json
vendored
Normal file
45
tests/fixtures/real_world/cpp/state/fopen_lifecycle.expect.json
vendored
Normal 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()"
|
||||
}
|
||||
]
|
||||
}
|
||||
11
tests/fixtures/real_world/cpp/state/malloc_branches.cpp
vendored
Normal file
11
tests/fixtures/real_world/cpp/state/malloc_branches.cpp
vendored
Normal 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
|
||||
}
|
||||
24
tests/fixtures/real_world/cpp/state/malloc_branches.expect.json
vendored
Normal file
24
tests/fixtures/real_world/cpp/state/malloc_branches.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
18
tests/fixtures/real_world/cpp/state/new_delete.cpp
vendored
Normal file
18
tests/fixtures/real_world/cpp/state/new_delete.cpp
vendored
Normal 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;
|
||||
}
|
||||
34
tests/fixtures/real_world/cpp/state/new_delete.expect.json
vendored
Normal file
34
tests/fixtures/real_world/cpp/state/new_delete.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
tests/fixtures/real_world/cpp/state/smart_ptr_vs_raw.cpp
vendored
Normal file
12
tests/fixtures/real_world/cpp/state/smart_ptr_vs_raw.cpp
vendored
Normal 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
|
||||
}
|
||||
24
tests/fixtures/real_world/cpp/state/smart_ptr_vs_raw.expect.json
vendored
Normal file
24
tests/fixtures/real_world/cpp/state/smart_ptr_vs_raw.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
16
tests/fixtures/real_world/cpp/taint/cmdi_system.cpp
vendored
Normal file
16
tests/fixtures/real_world/cpp/taint/cmdi_system.cpp
vendored
Normal 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);
|
||||
}
|
||||
}
|
||||
45
tests/fixtures/real_world/cpp/taint/cmdi_system.expect.json
vendored
Normal file
45
tests/fixtures/real_world/cpp/taint/cmdi_system.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
9
tests/fixtures/real_world/cpp/taint/env_to_system.cpp
vendored
Normal file
9
tests/fixtures/real_world/cpp/taint/env_to_system.cpp
vendored
Normal 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;
|
||||
}
|
||||
34
tests/fixtures/real_world/cpp/taint/env_to_system.expect.json
vendored
Normal file
34
tests/fixtures/real_world/cpp/taint/env_to_system.expect.json
vendored
Normal 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()"
|
||||
}
|
||||
]
|
||||
}
|
||||
10
tests/fixtures/real_world/cpp/taint/format_string.cpp
vendored
Normal file
10
tests/fixtures/real_world/cpp/taint/format_string.cpp
vendored
Normal 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);
|
||||
}
|
||||
23
tests/fixtures/real_world/cpp/taint/format_string.expect.json
vendored
Normal file
23
tests/fixtures/real_world/cpp/taint/format_string.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
tests/fixtures/real_world/cpp/taint/gets_strcpy.cpp
vendored
Normal file
12
tests/fixtures/real_world/cpp/taint/gets_strcpy.cpp
vendored
Normal 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);
|
||||
}
|
||||
34
tests/fixtures/real_world/cpp/taint/gets_strcpy.expect.json
vendored
Normal file
34
tests/fixtures/real_world/cpp/taint/gets_strcpy.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
13
tests/fixtures/real_world/cpp/taint/popen_cmd.cpp
vendored
Normal file
13
tests/fixtures/real_world/cpp/taint/popen_cmd.cpp
vendored
Normal 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);
|
||||
}
|
||||
23
tests/fixtures/real_world/cpp/taint/popen_cmd.expect.json
vendored
Normal file
23
tests/fixtures/real_world/cpp/taint/popen_cmd.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
12
tests/fixtures/real_world/cpp/taint/reinterpret_cast.cpp
vendored
Normal file
12
tests/fixtures/real_world/cpp/taint/reinterpret_cast.cpp
vendored
Normal 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);
|
||||
}
|
||||
34
tests/fixtures/real_world/cpp/taint/reinterpret_cast.expect.json
vendored
Normal file
34
tests/fixtures/real_world/cpp/taint/reinterpret_cast.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
23
tests/fixtures/real_world/go/cfg/defer_close.expect.json
vendored
Normal file
23
tests/fixtures/real_world/go/cfg/defer_close.expect.json
vendored
Normal 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()"
|
||||
}
|
||||
]
|
||||
}
|
||||
36
tests/fixtures/real_world/go/cfg/defer_close.go
vendored
Normal file
36
tests/fixtures/real_world/go/cfg/defer_close.go
vendored
Normal 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
|
||||
}
|
||||
47
tests/fixtures/real_world/go/cfg/error_handling.expect.json
vendored
Normal file
47
tests/fixtures/real_world/go/cfg/error_handling.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
38
tests/fixtures/real_world/go/cfg/error_handling.go
vendored
Normal file
38
tests/fixtures/real_world/go/cfg/error_handling.go
vendored
Normal 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()
|
||||
}
|
||||
24
tests/fixtures/real_world/go/cfg/panic_recover.expect.json
vendored
Normal file
24
tests/fixtures/real_world/go/cfg/panic_recover.expect.json
vendored
Normal 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()"
|
||||
}
|
||||
]
|
||||
}
|
||||
36
tests/fixtures/real_world/go/cfg/panic_recover.go
vendored
Normal file
36
tests/fixtures/real_world/go/cfg/panic_recover.go
vendored
Normal 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()
|
||||
}
|
||||
36
tests/fixtures/real_world/go/cfg/select_goroutine.expect.json
vendored
Normal file
36
tests/fixtures/real_world/go/cfg/select_goroutine.expect.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
||||
32
tests/fixtures/real_world/go/cfg/select_goroutine.go
vendored
Normal file
32
tests/fixtures/real_world/go/cfg/select_goroutine.go
vendored
Normal 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)
|
||||
}
|
||||
72
tests/fixtures/real_world/go/mixed/http_handler_full.expect.json
vendored
Normal file
72
tests/fixtures/real_world/go/mixed/http_handler_full.expect.json
vendored
Normal 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
Loading…
Add table
Add a link
Reference in a new issue