[pitboss] phase 22: Track F.2 + F.3 — Cross-language framework probes + data store / external service / dangerous-local detection

This commit is contained in:
pitboss 2026-05-15 13:28:58 -05:00
parent c03326a658
commit 2395446655
43 changed files with 5213 additions and 82 deletions

View file

@ -0,0 +1,13 @@
package main
import "github.com/gin-gonic/gin"
func main() {
r := gin.Default()
r.GET("/users", listUsers)
r.Run()
}
func listUsers(c *gin.Context) {
c.JSON(200, []string{})
}

View file

@ -0,0 +1,12 @@
package main
import "net/http"
func main() {
http.HandleFunc("/users", listUsers)
http.ListenAndServe(":8080", nil)
}
func listUsers(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("[]"))
}

View file

@ -0,0 +1,17 @@
package com.example;
import io.quarkus.runtime.Quarkus;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@ApplicationScoped
@Path("/api")
public class GreetResource {
@GET
@Path("/hello")
public String hello() {
return "hi";
}
}

View file

@ -0,0 +1,14 @@
package com.example;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@Path("/users")
public class UserResource {
@GET
@Path("/{id}")
public String get() {
return "{}";
}
}

View file

@ -0,0 +1,11 @@
package com.example;
@RestController
@RequestMapping("/api")
public class UserController {
@GetMapping("/users")
public String list() {
return "[]";
}
}

View file

@ -0,0 +1,8 @@
const express = require("express");
const app = express();
app.get("/users", (req, res) => {
res.send("ok");
});
app.listen(3000);

View file

@ -0,0 +1,8 @@
const Router = require("@koa/router");
const router = new Router();
router.get("/users", async (ctx) => {
ctx.body = [];
});
module.exports = router;

View file

@ -0,0 +1,3 @@
<?php
Route::get('/users', 'UserController@index');

View file

@ -0,0 +1,3 @@
<?php
$app->get('/users', 'UsersController:list');

View file

@ -0,0 +1,10 @@
from django.urls import path
def admin_view(request):
return None
urlpatterns = [
path("admin/", admin_view),
]

View file

@ -0,0 +1,8 @@
from fastapi import FastAPI
app = FastAPI()
@app.get("/items")
def list_items():
return []

View file

@ -0,0 +1,8 @@
from flask import Flask
app = Flask(__name__)
@app.get("/users")
def list_users():
return "ok"

View file

@ -0,0 +1,9 @@
class UsersController < ApplicationController
def index
render json: []
end
def show
render json: {}
end
end

View file

@ -0,0 +1,5 @@
require 'sinatra'
get '/users' do
'[]'
end

View file

@ -0,0 +1,6 @@
use actix_web::{get, HttpResponse};
#[get("/users")]
async fn list_users() -> HttpResponse {
HttpResponse::Ok().finish()
}

View file

@ -0,0 +1,9 @@
use axum::{routing::get, Router};
async fn list_users() -> &'static str {
"[]"
}
fn app() -> Router {
Router::new().route("/users", get(list_users))
}

View file

@ -0,0 +1,3 @@
export async function GET(req: Request): Promise<Response> {
return new Response("ok");
}

208
tests/surface_cross_lang.rs Normal file
View file

@ -0,0 +1,208 @@
//! Phase 22 — cross-language `SurfaceMap` framework probes.
//!
//! One fixture per (language, framework) pair under
//! `tests/dynamic_fixtures/surface/<probe>/`. Each probe is exercised
//! through the public [`build_surface_map`] entry point and asserted
//! on:
//!
//! 1. At least one [`SurfaceNode::EntryPoint`] is emitted.
//! 2. The recognised entry-point carries the expected [`Framework`]
//! tag.
//! 3. The recognised entry-point's `route` field contains the expected
//! substring (the path declared in the fixture).
use nyx_scanner::callgraph::CallGraph;
use nyx_scanner::summary::GlobalSummaries;
use nyx_scanner::surface::{
Framework, SurfaceMap, SurfaceNode,
build::{build_surface_map, SurfaceBuildInputs},
};
use nyx_scanner::utils::config::Config;
use std::path::{Path, PathBuf};
const FIXTURE_ROOT: &str = "tests/dynamic_fixtures/surface";
fn empty_call_graph() -> CallGraph {
CallGraph {
graph: petgraph::graph::DiGraph::new(),
index: Default::default(),
unresolved_not_found: vec![],
unresolved_ambiguous: vec![],
}
}
fn build(fixture_dir: &str) -> SurfaceMap {
let dir = Path::new(FIXTURE_ROOT).join(fixture_dir);
let mut files: Vec<PathBuf> = Vec::new();
walk(&dir, &mut files);
let cfg = Config::default();
let gs = GlobalSummaries::new();
let cg = empty_call_graph();
let inputs = SurfaceBuildInputs {
files: &files,
scan_root: Some(&dir),
global_summaries: &gs,
call_graph: &cg,
config: &cfg,
};
build_surface_map(&inputs)
}
fn walk(dir: &Path, out: &mut Vec<PathBuf>) {
let entries = match std::fs::read_dir(dir) {
Ok(e) => e,
Err(_) => return,
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
walk(&path, out);
} else {
out.push(path);
}
}
}
fn assert_entry(map: &SurfaceMap, framework: Framework, route_substr: &str) {
let routes: Vec<String> = map
.nodes
.iter()
.filter_map(|n| match n {
SurfaceNode::EntryPoint(ep) if ep.framework == framework => Some(ep.route.clone()),
_ => None,
})
.collect();
assert!(
!routes.is_empty(),
"no entry-point with framework {:?} found in map = {:#?}",
framework,
map.nodes
);
assert!(
routes.iter().any(|r| r.contains(route_substr)),
"expected a route containing {route_substr:?}; got {routes:?}",
);
}
#[test]
fn python_flask_fixture() {
let map = build("python_flask");
assert_entry(&map, Framework::Flask, "/users");
}
#[test]
fn python_fastapi_fixture() {
let map = build("python_fastapi");
assert_entry(&map, Framework::FastApi, "/items");
}
#[test]
fn python_django_fixture() {
let map = build("python_django");
assert_entry(&map, Framework::Django, "admin");
}
#[test]
fn js_express_fixture() {
let map = build("js_express");
assert_entry(&map, Framework::Express, "/users");
}
#[test]
fn js_koa_fixture() {
let map = build("js_koa");
// koa probe currently emits the Express variant tag because the
// SurfaceMap framework taxonomy folds koa-router under the
// generic "node http microframework" bucket. See
// [`nyx_scanner::surface::lang::js_koa`] doc comment.
assert_entry(&map, Framework::Express, "/users");
}
#[test]
fn ts_next_fixture() {
let map = build("ts_next");
assert_entry(&map, Framework::NextAppRouter, "users");
}
#[test]
fn java_spring_fixture() {
let map = build("java_spring");
assert_entry(&map, Framework::Spring, "/api/users");
}
#[test]
fn java_servlet_fixture() {
let map = build("java_servlet");
assert_entry(&map, Framework::JaxRs, "/users");
}
#[test]
fn java_quarkus_fixture() {
let map = build("java_quarkus");
assert_entry(&map, Framework::JaxRs, "/api/hello");
}
#[test]
fn go_http_fixture() {
let map = build("go_http");
assert_entry(&map, Framework::NetHttp, "/users");
}
#[test]
fn go_gin_fixture() {
let map = build("go_gin");
assert_entry(&map, Framework::Gin, "/users");
}
#[test]
fn php_laravel_fixture() {
let map = build("php_laravel");
// Laravel folds into the generic Sinatra-like framework bucket
// because the SurfaceMap framework taxonomy is method-call shaped
// rather than per-stack. See `surface::lang::php_laravel`.
assert_entry(&map, Framework::Sinatra, "/users");
}
#[test]
fn php_slim_fixture() {
let map = build("php_slim");
assert_entry(&map, Framework::Sinatra, "/users");
}
#[test]
fn ruby_sinatra_fixture() {
let map = build("ruby_sinatra");
assert_entry(&map, Framework::Sinatra, "/users");
}
#[test]
fn ruby_rails_fixture() {
let map = build("ruby_rails");
// Controller actions have empty routes because the route table
// lives in `config/routes.rb` (separate file). Assert on the
// handler name surfacing instead.
let handlers: Vec<String> = map
.nodes
.iter()
.filter_map(|n| match n {
SurfaceNode::EntryPoint(ep) if ep.framework == Framework::Rails => {
Some(ep.handler_name.clone())
}
_ => None,
})
.collect();
assert!(handlers.contains(&"index".to_string()));
assert!(handlers.contains(&"show".to_string()));
}
#[test]
fn rust_actix_fixture() {
let map = build("rust_actix");
assert_entry(&map, Framework::Actix, "/users");
}
#[test]
fn rust_axum_fixture() {
let map = build("rust_axum");
assert_entry(&map, Framework::Axum, "/users");
}