* chore: Exclude CLAUDE.md from Cargo.toml

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

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

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

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

* feat: enhance diagnostic output formatting and add evidence structure

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

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

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

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

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

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

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

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

View file

@ -0,0 +1,25 @@
{
"description": "cURL handle resource leak - missing curl_close vs properly closed",
"tags": [
"cfg",
"resource-leak",
"curl",
"php"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cfg-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
1,
9
],
"evidence_contains": [],
"notes": "fetchUrl opens cURL handle but never calls curl_close"
}
]
}

View file

@ -0,0 +1,17 @@
<?php
function fetchUrl($url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
// Missing curl_close($ch)
return $response;
}
function fetchUrlSafe($url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return $response;
}
?>

View file

@ -0,0 +1,37 @@
{
"description": "Error check logs but falls through to dangerous system() call with null result",
"tags": [
"cfg",
"error-fallthrough",
"cmdi",
"php"
],
"modes": [
"full",
"ast"
],
"expected": [
{
"rule_id": "cfg-error-fallthrough",
"severity": null,
"must_match": false,
"line_range": [
1,
10
],
"evidence_contains": [],
"notes": "Error condition logs but does not return, falls through to system() call"
},
{
"rule_id": "php.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
6,
10
],
"evidence_contains": [],
"notes": "system() called with potentially null array access"
}
]
}

View file

@ -0,0 +1,10 @@
<?php
function processRequest($data) {
$result = json_decode($data, true);
if ($result === null) {
error_log("Invalid JSON");
// falls through!
}
system($result['command']);
}
?>

View file

@ -0,0 +1,38 @@
{
"description": "Switch/case dispatching to dangerous functions eval and system",
"tags": [
"cfg",
"code-exec",
"cmdi",
"switch",
"php"
],
"modes": [
"full",
"ast"
],
"expected": [
{
"rule_id": "php.code_exec.eval",
"severity": null,
"must_match": true,
"line_range": [
3,
7
],
"evidence_contains": [],
"notes": "eval() called in switch case with function parameter"
},
{
"rule_id": "php.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
6,
10
],
"evidence_contains": [],
"notes": "system() called in switch case with function parameter"
}
]
}

View file

@ -0,0 +1,17 @@
<?php
function handleAction($action, $input) {
switch ($action) {
case 'eval':
eval($input);
break;
case 'exec':
system($input);
break;
case 'log':
error_log($input);
break;
default:
echo "Unknown action";
}
}
?>

View file

@ -0,0 +1,26 @@
{
"description": "File handle resource management with try/catch/finally vs early return leak",
"tags": [
"cfg",
"resource-leak",
"try-finally",
"file-io",
"php"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "cfg-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
12,
24
],
"evidence_contains": [],
"notes": "leakyProcess returns null on empty data without closing file handle"
}
]
}

View file

@ -0,0 +1,23 @@
<?php
function processFile($path) {
$fh = fopen($path, 'r');
try {
$data = fread($fh, filesize($path));
return $data;
} catch (Exception $e) {
echo $e->getMessage();
} finally {
fclose($fh);
}
}
function leakyProcess($path) {
$fh = fopen($path, 'r');
$data = fread($fh, filesize($path));
if (empty($data)) {
return null; // fh leaked
}
fclose($fh);
return $data;
}
?>

View file

@ -0,0 +1,37 @@
{
"description": "File upload with command injection via unsanitized filename in system() call",
"tags": [
"taint",
"cmdi",
"file-upload",
"php",
"mixed"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
1,
8
],
"evidence_contains": [],
"notes": "$_FILES filename flows through concatenation into system() chmod call"
},
{
"rule_id": "php.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
4,
8
],
"evidence_contains": [],
"notes": "system() with string concatenation containing user-controlled filename"
}
]
}

View file

@ -0,0 +1,8 @@
<?php
$filename = $_FILES['file']['name'];
$tmp = $_FILES['file']['tmp_name'];
$target = '/var/www/uploads/' . $filename;
move_uploaded_file($tmp, $target);
system('chmod 644 ' . $target); // command injection via filename!
echo "File uploaded: " . htmlspecialchars($filename);
?>

View file

@ -0,0 +1,61 @@
{
"description": "Web handler dispatches to system, eval, and fopen based on user input",
"tags": [
"taint",
"state",
"cmdi",
"code-exec",
"resource-leak",
"php",
"mixed"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "php.cmdi.system",
"severity": null,
"must_match": true,
"line_range": [
4,
8
],
"evidence_contains": [],
"notes": "system() called with $_POST data"
},
{
"rule_id": "php.code_exec.eval",
"severity": null,
"must_match": true,
"line_range": [
6,
10
],
"evidence_contains": [],
"notes": "eval() called with $_POST data"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
1,
8
],
"evidence_contains": [],
"notes": "$_POST['data'] flows into system()"
},
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
8,
14
],
"evidence_contains": [],
"notes": "File handle opened in read branch but never closed"
}
]
}

View file

@ -0,0 +1,14 @@
<?php
$action = $_GET['action'];
$input = $_POST['data'];
if ($action === 'exec') {
system($input);
} elseif ($action === 'eval') {
eval($input);
} elseif ($action === 'read') {
$fh = fopen($input, 'r');
echo fread($fh, 4096);
// fh leaked
}
?>

View file

@ -0,0 +1,26 @@
{
"description": "File handle leaked in else branch of conditional",
"tags": [
"state",
"resource-leak",
"branch",
"file-io",
"php"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-resource-leak-possible",
"severity": null,
"must_match": false,
"line_range": [
1,
14
],
"evidence_contains": [],
"notes": "File handle closed in if branch but leaked in else branch"
}
]
}

View file

@ -0,0 +1,13 @@
<?php
function conditionalClose($path, $flag) {
$fh = fopen($path, 'r');
if ($flag) {
$data = fread($fh, 1024);
fclose($fh);
return $data;
} else {
return "skipped";
// $fh leaked
}
}
?>

View file

@ -0,0 +1,25 @@
{
"description": "cURL handle used after being closed",
"tags": [
"state",
"use-after-close",
"curl",
"php"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-use-after-close",
"severity": null,
"must_match": false,
"line_range": [
1,
9
],
"evidence_contains": [],
"notes": "curl_exec called after curl_close on same handle"
}
]
}

View file

@ -0,0 +1,9 @@
<?php
function useAfterClose($url) {
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_close($ch);
$response = curl_exec($ch); // use after close!
return $response;
}
?>

View file

@ -0,0 +1,25 @@
{
"description": "mysqli connection resource leak - missing close vs properly closed",
"tags": [
"state",
"resource-leak",
"mysqli",
"php"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
1,
8
],
"evidence_contains": [],
"notes": "queryUnsafe creates mysqli connection but never calls close()"
}
]
}

View file

@ -0,0 +1,15 @@
<?php
function queryUnsafe() {
$conn = new mysqli("localhost", "user", "pass", "db");
$result = $conn->query("SELECT 1");
return $result;
// $conn never closed
}
function querySafe() {
$conn = new mysqli("localhost", "user", "pass", "db");
$result = $conn->query("SELECT 1");
$conn->close();
return $result;
}
?>

View file

@ -0,0 +1,37 @@
{
"description": "PHP file handle lifecycle: leak, proper close, double close",
"tags": [
"state",
"resource-leak",
"double-close",
"file-io",
"php"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "state-resource-leak",
"severity": null,
"must_match": false,
"line_range": [
1,
7
],
"evidence_contains": [],
"notes": "readAndLeak opens file handle but never calls fclose"
},
{
"rule_id": "state-double-close",
"severity": null,
"must_match": false,
"line_range": [
13,
21
],
"evidence_contains": [],
"notes": "doubleClose calls fclose twice on same handle"
}
]
}

View file

@ -0,0 +1,20 @@
<?php
function readAndLeak($path) {
$fh = fopen($path, 'r');
$data = fread($fh, 1024);
return $data;
}
function readAndClose($path) {
$fh = fopen($path, 'r');
$data = fread($fh, 1024);
fclose($fh);
return $data;
}
function doubleClose($path) {
$fh = fopen($path, 'r');
fclose($fh);
fclose($fh);
}
?>

View file

@ -0,0 +1,25 @@
{
"description": "Command injection via shell_exec with unsanitized GET parameter",
"tags": [
"taint",
"cmdi",
"shell_exec",
"php"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
1,
6
],
"evidence_contains": [],
"notes": "$_GET['cmd'] flows directly into shell_exec without sanitization"
}
]
}

View file

@ -0,0 +1,10 @@
<?php
$cmd = $_GET['cmd'];
$output = shell_exec($cmd);
echo $output;
// Safe version
$safe_cmd = escapeshellarg($_GET['safe_cmd']);
$safe_output = shell_exec('echo ' . $safe_cmd);
echo $safe_output;
?>

View file

@ -0,0 +1,36 @@
{
"description": "Unsafe deserialization of user-controlled cookie data via unserialize",
"tags": [
"taint",
"deser",
"unserialize",
"php"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "php.deser.unserialize",
"severity": null,
"must_match": true,
"line_range": [
1,
5
],
"evidence_contains": [],
"notes": "unserialize() on cookie data enables object injection"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
1,
5
],
"evidence_contains": [],
"notes": "$_COOKIE['session_data'] flows into unserialize()"
}
]
}

View file

@ -0,0 +1,10 @@
<?php
$data = $_COOKIE['session_data'];
$obj = unserialize($data);
echo $obj->name;
// Safe: JSON instead
$json_data = $_COOKIE['json_data'];
$safe_obj = json_decode($json_data);
echo $safe_obj->name;
?>

View file

@ -0,0 +1,58 @@
{
"description": "eval() called with user-controlled POST and GET input",
"tags": [
"taint",
"code-exec",
"eval",
"php"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "php.code_exec.eval",
"severity": null,
"must_match": true,
"line_range": [
1,
5
],
"evidence_contains": [],
"notes": "eval() with $_POST['code'] - AST pattern match"
},
{
"rule_id": "php.code_exec.eval",
"severity": null,
"must_match": true,
"line_range": [
4,
8
],
"evidence_contains": [],
"notes": "eval() with $_GET['expr'] concatenated - AST pattern match"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
1,
5
],
"evidence_contains": [],
"notes": "$_POST['code'] flows directly into eval()"
},
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
3,
8
],
"evidence_contains": [],
"notes": "$_GET['expr'] concatenated and flows into eval()"
}
]
}

View file

@ -0,0 +1,8 @@
<?php
$code = $_POST['code'];
eval($code);
$expr = $_GET['expr'];
$result = eval('return ' . $expr . ';');
echo $result;
?>

View file

@ -0,0 +1,25 @@
{
"description": "Unsafe file upload with user-controlled filename used in move_uploaded_file",
"tags": [
"taint",
"file-upload",
"path-traversal",
"php"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
1,
6
],
"evidence_contains": [],
"notes": "$_FILES['upload']['name'] flows into file path without sanitization"
}
]
}

View file

@ -0,0 +1,12 @@
<?php
$filename = $_FILES['upload']['name'];
$target = '/uploads/' . $filename;
move_uploaded_file($_FILES['upload']['tmp_name'], $target);
echo "Uploaded to: " . $target;
// Safe version
$safe_name = basename($_FILES['upload']['name']);
$safe_name = preg_replace('/[^a-zA-Z0-9._-]/', '', $safe_name);
$safe_target = '/uploads/' . $safe_name;
move_uploaded_file($_FILES['upload']['tmp_name'], $safe_target);
?>

View file

@ -0,0 +1,25 @@
{
"description": "Remote file inclusion via user-controlled include path",
"tags": [
"taint",
"rfi",
"include",
"php"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
1,
5
],
"evidence_contains": [],
"notes": "$_GET['page'] flows directly into include() without whitelist check"
}
]
}

View file

@ -0,0 +1,11 @@
<?php
$page = $_GET['page'];
include($page . '.php');
// Safe version
$allowed = ['home', 'about', 'contact'];
$safe_page = $_GET['page'];
if (in_array($safe_page, $allowed)) {
include($safe_page . '.php');
}
?>

View file

@ -0,0 +1,25 @@
{
"description": "SQL injection via string concatenation with user input in mysqli query",
"tags": [
"taint",
"sqli",
"mysqli",
"php"
],
"modes": [
"full"
],
"expected": [
{
"rule_id": "taint-unsanitised-flow",
"severity": null,
"must_match": true,
"line_range": [
1,
6
],
"evidence_contains": [],
"notes": "$_GET['id'] concatenated directly into SQL query string"
}
]
}

View file

@ -0,0 +1,13 @@
<?php
$conn = new mysqli("localhost", "user", "pass", "db");
$id = $_GET['id'];
$result = $conn->query("SELECT * FROM users WHERE id = " . $id);
while ($row = $result->fetch_assoc()) {
echo $row['name'];
}
// Safe version
$stmt = $conn->prepare("SELECT * FROM users WHERE id = ?");
$stmt->bind_param("s", $_GET['safe_id']);
$stmt->execute();
?>