feat(detection): language-scoped rules + multi-language benchmark corpora

Rebuild the quick-scan detection engine and ground-truth benchmarks to hit
>=90% recall / <10% false positives for C/C++, Python, JavaScript, and Java.

Engine (findings.py, preprocess.py):
- Scope each PatternRule by language (fixes cross-language false positives such
  as the Python eval/exec rule firing on JS .exec()).
- Add JavaScript/TypeScript rules (command exec, eval/Function, reflected XSS,
  SQLi, path traversal, SSRF, weak hash, deserialization); execFile/spawn-argv
  excluded as safe alternatives.
- Add Java rules (Runtime exec, SQL string concatenation, weak hash, readObject)
  and a Groovy server-side template rule (yieldUnescaped of a bound variable).
- Broaden C/C++ and Python coverage; precision guards for format strings and
  parameterised vs concatenated SQL; #-comment skipping; CWE in rationale.
- Register .java/.groovy/.tpl source extensions.

Benchmarks:
- Add realistic corpora modeled on Damn Vulnerable C/C++, PyGoat/DVPWA, and
  NodeGoat, plus the real kiview/damn-vulnerable-spring-boot-app vendored
  verbatim (MIT, attribution in LICENSE/NOTICE.md). Each manifest carries
  complete, independently-labeled ground truth including genuinely
  hard-to-detect vulns (integer overflow, second-order SQLi, prototype
  pollution) so recall is measured honestly, plus safe-API files for precision.
- Fix pre-existing smoke-manifest ground-truth errors (Python/JS line offsets,
  missing C/C++ strcpy entry, Java SQLi line).

benchmark.py:
- Fix sink matching to normalise the expected sink consistently so sinks with
  underscores (e.g. render_template_string) can match.

Tests:
- Add tests/test_detection_benchmarks.py enforcing the >=90% recall / <10% FP
  goal per language and overall.

Measured: C/C++ 93.3%, Python 93.3%, JavaScript 92.3%, Java 100%;
overall 93.6% recall, 0.0% false-positive rate.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Marius Ciepluch
2026-06-21 11:49:31 +02:00
co-authored by Claude Opus 4.8
parent be65aed96e
commit cf9f945992
38 changed files with 2420 additions and 15 deletions
@@ -0,0 +1,13 @@
package com.groovycoder.dvsba;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DvsbaApplication {
public static void main(String[] args) {
SpringApplication.run(DvsbaApplication.class, args);
}
}
@@ -0,0 +1,14 @@
package com.groovycoder.dvsba;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
@Controller
public class HomeController {
@GetMapping("/")
public ModelAndView home() {
return new ModelAndView("views/home");
}
}
@@ -0,0 +1,13 @@
package com.groovycoder.dvsba.books;
public class Book {
Long id;
String name;
String author;
public Book(Long id, String name, String author) {
this.id = id;
this.name = name;
this.author = author;
}
}
@@ -0,0 +1,88 @@
package com.groovycoder.dvsba.books;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.ResultSetExtractor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import javax.annotation.PostConstruct;
import java.util.*;
@RequestMapping("/books")
@Controller
class BookController {
private final JdbcTemplate jdbcTemplate;
public BookController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@RequestMapping("/")
public ModelAndView home() {
List<Book> books = loadBooks();
Map<String, Object> model = new HashMap<>();
model.put("books", books);
return new ModelAndView("views/sql/home", model);
}
@RequestMapping("/detail")
public ModelAndView detail(@RequestParam(value = "id") String id) {
String sql = "SELECT * FROM books WHERE id=" + id;
final Book[] book = new Book[1];
jdbcTemplate.query(sql, (ResultSetExtractor) rs -> {
if (rs.next())
book[0] = new Book(rs.getLong(1), rs.getString(2), rs.getString(3));
return null;
});
Map<String, Object> model = new HashMap<>();
model.put("book", book[0]);
return new ModelAndView("views/sql/detail", model);
}
@PostConstruct
private void bootstrap() {
initDb();
List<Book> books = Arrays.asList(
new Book(1L, "Moby Dick", "Herman Melville"),
new Book(2L, "Unsichtbare Spuren", "Andreas Franz"),
new Book(3L, "Das Paket", "Sebastian Fitzek")
);
saveBooks(books);
}
private void saveBooks(List<Book> books) {
books.forEach(book ->
jdbcTemplate.update("INSERT INTO books (id, `name`, author) values (?, ?, ?)",
book.id, book.name, book.author)
);
}
private List<Book> loadBooks() {
return jdbcTemplate.query("SELECT * FROM books", rs -> {
List<Book> books = new LinkedList<>();
while (rs.next()) {
books.add(new Book(rs.getLong(1), rs.getString(2), rs.getString(3)));
}
return books;
});
}
private void initDb() {
jdbcTemplate.execute("CREATE TABLE books (id NUMBER, name VARCHAR(255), author VARCHAR(255))");
}
}
@@ -0,0 +1,47 @@
package com.groovycoder.dvsba.comments;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Controller
@RequestMapping("/comments")
public class CommentController {
private List<String> comments = new ArrayList<>(Arrays.asList(
"Hi super Webseite leider keine Mobile App deswegen nur einen Stern.",
"Ich muss darauf hinweisen, dass eine Webseite ohne Impressum zu betreiben !!!strafbar!!! ist!!!",
"Hallo, wir sinds! Wolltest du nicht die Fotos von Samstag hochladen? Grüße!"));
@GetMapping(path = "/")
public ModelAndView listComments() {
Map<String, Object> model = new HashMap<>();
model.put("comments", comments);
return new ModelAndView(
"views/comment/comment",
model);
}
@PostMapping("/")
public ModelAndView postComment(@RequestParam String input) {
comments.add(input);
Map<String, Object> model = new HashMap<>();
model.put("comments", comments);
return new ModelAndView(
"views/comment/comment",
model);
}
}
@@ -0,0 +1,25 @@
yieldUnescaped '<!DOCTYPE html>'
html {
head {
title('Spring Boot - Groovy templates example')
link(rel: 'stylesheet', href: '/css/bootstrap.min.css')
}
body {
form(action: "/comments/", method: "POST") {
input(name: "input") {
}
}
comments.each { comment ->
div {
h3(style: "border-bottom: 1px solid black;") {
yield "Comment by Anonymous"
}
div(style: "border-bottom: 1px solid black;") {
yieldUnescaped comment
}
}
}
}
}
@@ -0,0 +1,13 @@
yieldUnescaped '<!DOCTYPE html>'
html {
head {
title('Damn Vulnerable Spring Boot App - Home')
}
body {
h1 {
yield 'Hello and Welcome to the DVSBA!'
}
p ("Take a look around! We have a ${$a(href:'/books/', "catalouge")} of our favourite books which you can browse. If you like it, leave a comment in the ${ $a(href: '/comments/', "comment section")}!")
}
}
@@ -0,0 +1,16 @@
yieldUnescaped '<!DOCTYPE html>'
html {
head {
title('Damn Vulnerable Spring Boot App')
}
body {
div(class: 'container') {
a(class: 'brand',
href: '/books/',
'List')
br()
br()
p "ID: ${book.id}, Name: ${book.name}, Author: ${book.author}"
}
}
}
@@ -0,0 +1,38 @@
yieldUnescaped '<!DOCTYPE html>'
html {
head {
title('Damn Vulnerable Spring Boot App')
}
body {
div(class: 'container') {
form(method: "GET", action: "/books/detail") {
input(name: "id", placeholder: "Search for a book id!")
}
br
table(border:1) {
thead {
tr {
td 'ID'
td 'Name'
td 'Author'
}
}
tbody {
books.each { book ->
tr {
td {
a(href: "/books/detail?id=${book.id}", "${book.id}")
}
td {
a(href: "/books/detail?id=${book.id}", "${book.name}")
}
td {
a(href: "/books/detail?id=${book.id}", "${book.author}")
}
}
}
}
}
}
}
}