* 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

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

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

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

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

View file

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

View file

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

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

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

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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