mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-21 20:18:06 +02:00
[pitboss] phase 13: Track L.11 — Express / Koa / NestJS / Fastify adapters
This commit is contained in:
parent
9ed837be9b
commit
04bf7b997f
27 changed files with 2670 additions and 11 deletions
28
tests/dynamic_fixtures/js_frameworks/express/benign.js
Normal file
28
tests/dynamic_fixtures/js_frameworks/express/benign.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Phase 13 (Track L.11) — Express CMDI benign fixture.
|
||||
//
|
||||
// The `/run` route accepts a `cmd` query parameter but rejects
|
||||
// everything outside an allowlist before invoking `child_process.exec`
|
||||
// with a fixed argv, so the sink call is unreachable for
|
||||
// attacker-controlled values.
|
||||
|
||||
const express = require('express');
|
||||
const { execFile } = require('child_process');
|
||||
|
||||
const app = express();
|
||||
|
||||
const ALLOW = new Set(['status', 'uptime', 'version']);
|
||||
|
||||
function runCmd(req, res) {
|
||||
const cmd = req.query.cmd || '';
|
||||
if (!ALLOW.has(cmd)) {
|
||||
return res.status(400).send('rejected');
|
||||
}
|
||||
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
|
||||
if (err) return res.status(500).send(String(err));
|
||||
res.send(stdout);
|
||||
});
|
||||
}
|
||||
|
||||
app.get('/run', runCmd);
|
||||
|
||||
module.exports = { app, runCmd };
|
||||
23
tests/dynamic_fixtures/js_frameworks/express/vuln.js
Normal file
23
tests/dynamic_fixtures/js_frameworks/express/vuln.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Phase 13 (Track L.11) — Express CMDI vuln fixture.
|
||||
//
|
||||
// The `/run` route forwards a `cmd` query parameter straight into
|
||||
// `child_process.exec`, so any attacker who reaches the route can
|
||||
// execute arbitrary shell. Adapter binding:
|
||||
// `app.get('/run', runCmd)` with `cmd` flowing through `req.query.cmd`.
|
||||
|
||||
const express = require('express');
|
||||
const { exec } = require('child_process');
|
||||
|
||||
const app = express();
|
||||
|
||||
function runCmd(req, res) {
|
||||
const cmd = req.query.cmd || '';
|
||||
exec(cmd, (err, stdout) => {
|
||||
if (err) return res.status(500).send(String(err));
|
||||
res.send(stdout);
|
||||
});
|
||||
}
|
||||
|
||||
app.get('/run', runCmd);
|
||||
|
||||
module.exports = { app, runCmd };
|
||||
28
tests/dynamic_fixtures/js_frameworks/fastify/benign.js
Normal file
28
tests/dynamic_fixtures/js_frameworks/fastify/benign.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Phase 13 (Track L.11) — Fastify CMDI benign fixture.
|
||||
//
|
||||
// The `/run` route accepts a `cmd` query parameter but rejects
|
||||
// everything outside an allowlist before invoking
|
||||
// `child_process.execFile` with a fixed argv.
|
||||
|
||||
const fastify = require('fastify')();
|
||||
const { execFile } = require('child_process');
|
||||
|
||||
const ALLOW = new Set(['status', 'uptime', 'version']);
|
||||
|
||||
async function runCmd(request, reply) {
|
||||
const cmd = request.query.cmd || '';
|
||||
if (!ALLOW.has(cmd)) {
|
||||
reply.code(400).send('rejected');
|
||||
return;
|
||||
}
|
||||
const out = await new Promise((resolve) => {
|
||||
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
|
||||
resolve(err ? String(err) : stdout);
|
||||
});
|
||||
});
|
||||
reply.send(out);
|
||||
}
|
||||
|
||||
fastify.get('/run', runCmd);
|
||||
|
||||
module.exports = { app: fastify, runCmd };
|
||||
20
tests/dynamic_fixtures/js_frameworks/fastify/vuln.js
Normal file
20
tests/dynamic_fixtures/js_frameworks/fastify/vuln.js
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Phase 13 (Track L.11) — Fastify CMDI vuln fixture.
|
||||
//
|
||||
// The `/run` route forwards a `cmd` query parameter straight into
|
||||
// `child_process.exec`. Adapter binding: `fastify.get('/run', runCmd)`
|
||||
// with `cmd` flowing through `request.query.cmd`.
|
||||
|
||||
const fastify = require('fastify')();
|
||||
const { exec } = require('child_process');
|
||||
|
||||
async function runCmd(request, reply) {
|
||||
const cmd = request.query.cmd || '';
|
||||
const out = await new Promise((resolve) => {
|
||||
exec(cmd, (err, stdout) => resolve(err ? String(err) : stdout));
|
||||
});
|
||||
reply.send(out);
|
||||
}
|
||||
|
||||
fastify.get('/run', runCmd);
|
||||
|
||||
module.exports = { app: fastify, runCmd };
|
||||
34
tests/dynamic_fixtures/js_frameworks/koa/benign.js
Normal file
34
tests/dynamic_fixtures/js_frameworks/koa/benign.js
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Phase 13 (Track L.11) — Koa CMDI benign fixture.
|
||||
//
|
||||
// The `/run` route accepts a `cmd` query parameter but rejects
|
||||
// everything outside an allowlist before invoking `child_process.execFile`
|
||||
// with a fixed argv.
|
||||
|
||||
const Koa = require('koa');
|
||||
const Router = require('@koa/router');
|
||||
const { execFile } = require('child_process');
|
||||
|
||||
const app = new Koa();
|
||||
const router = new Router();
|
||||
|
||||
const ALLOW = new Set(['status', 'uptime', 'version']);
|
||||
|
||||
async function runCmd(ctx) {
|
||||
const cmd = ctx.query.cmd || '';
|
||||
if (!ALLOW.has(cmd)) {
|
||||
ctx.status = 400;
|
||||
ctx.body = 'rejected';
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => {
|
||||
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
|
||||
ctx.body = err ? String(err) : stdout;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
router.get('/run', runCmd);
|
||||
app.use(router.routes());
|
||||
|
||||
module.exports = { app, runCmd };
|
||||
27
tests/dynamic_fixtures/js_frameworks/koa/vuln.js
Normal file
27
tests/dynamic_fixtures/js_frameworks/koa/vuln.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Phase 13 (Track L.11) — Koa CMDI vuln fixture.
|
||||
//
|
||||
// The `/run` route forwards a `cmd` query parameter straight into
|
||||
// `child_process.exec`. Adapter binding: `router.get('/run', runCmd)`
|
||||
// with `cmd` flowing through `ctx.query.cmd`.
|
||||
|
||||
const Koa = require('koa');
|
||||
const Router = require('@koa/router');
|
||||
const { exec } = require('child_process');
|
||||
|
||||
const app = new Koa();
|
||||
const router = new Router();
|
||||
|
||||
async function runCmd(ctx) {
|
||||
const cmd = ctx.query.cmd || '';
|
||||
await new Promise((resolve) => {
|
||||
exec(cmd, (err, stdout) => {
|
||||
ctx.body = err ? String(err) : stdout;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
router.get('/run', runCmd);
|
||||
app.use(router.routes());
|
||||
|
||||
module.exports = { app, runCmd };
|
||||
26
tests/dynamic_fixtures/js_frameworks/nest/benign.js
Normal file
26
tests/dynamic_fixtures/js_frameworks/nest/benign.js
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Phase 13 (Track L.11) — NestJS CMDI benign fixture. Same adapter
|
||||
// binding shape as the vuln fixture; the differential outcome is what
|
||||
// distinguishes the two.
|
||||
|
||||
require('reflect-metadata');
|
||||
const { Controller, Get, Query } = require('@nestjs/common');
|
||||
const { execFile } = require('child_process');
|
||||
|
||||
const ALLOW = new Set(['status', 'uptime', 'version']);
|
||||
|
||||
@Controller('')
|
||||
class AppController {
|
||||
@Get('run')
|
||||
runCmd(@Query('cmd') cmd) {
|
||||
if (!ALLOW.has(cmd || '')) {
|
||||
return 'rejected';
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
|
||||
resolve(err ? String(err) : stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { AppController };
|
||||
27
tests/dynamic_fixtures/js_frameworks/nest/vuln.js
Normal file
27
tests/dynamic_fixtures/js_frameworks/nest/vuln.js
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Phase 13 (Track L.11) — NestJS CMDI vuln fixture (Babel-stage-1
|
||||
// decorator syntax form). Real Nest projects publish their
|
||||
// controllers either as `.ts` files or as Babel-transpiled `.js`
|
||||
// carrying the inline decorator syntax via `@babel/plugin-proposal-decorators`
|
||||
// + `reflect-metadata`. The adapter binds the decorator syntax;
|
||||
// the harness loads the entry via `Test.createTestingModule`.
|
||||
//
|
||||
// Adapter binding: `@Controller('')` + `@Get('run')` on
|
||||
// `AppController.runCmd` with `cmd` flowing through `@Query('cmd')`.
|
||||
|
||||
require('reflect-metadata');
|
||||
const { Controller, Get, Query } = require('@nestjs/common');
|
||||
const { exec } = require('child_process');
|
||||
|
||||
@Controller('')
|
||||
class AppController {
|
||||
@Get('run')
|
||||
runCmd(@Query('cmd') cmd) {
|
||||
return new Promise((resolve) => {
|
||||
exec(cmd || '', (err, stdout) => {
|
||||
resolve(err ? String(err) : stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { AppController };
|
||||
27
tests/dynamic_fixtures/ts_frameworks/express/benign.ts
Normal file
27
tests/dynamic_fixtures/ts_frameworks/express/benign.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
// Phase 13 (Track L.11) — Express CMDI benign fixture (TypeScript).
|
||||
|
||||
import express, { Request, Response } from 'express';
|
||||
import { execFile } from 'child_process';
|
||||
|
||||
const app = express();
|
||||
|
||||
const ALLOW = new Set(['status', 'uptime', 'version']);
|
||||
|
||||
function runCmd(req: Request, res: Response) {
|
||||
const cmd = (req.query.cmd as string) || '';
|
||||
if (!ALLOW.has(cmd)) {
|
||||
res.status(400).send('rejected');
|
||||
return;
|
||||
}
|
||||
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
|
||||
if (err) {
|
||||
res.status(500).send(String(err));
|
||||
return;
|
||||
}
|
||||
res.send(stdout);
|
||||
});
|
||||
}
|
||||
|
||||
app.get('/run', runCmd);
|
||||
|
||||
export { app, runCmd };
|
||||
23
tests/dynamic_fixtures/ts_frameworks/express/vuln.ts
Normal file
23
tests/dynamic_fixtures/ts_frameworks/express/vuln.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Phase 13 (Track L.11) — Express CMDI vuln fixture (TypeScript).
|
||||
// Same shape as the JS twin; binds `app.get('/run', runCmd)` and
|
||||
// flows `req.query.cmd` straight into `exec`.
|
||||
|
||||
import express, { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
const app = express();
|
||||
|
||||
function runCmd(req: Request, res: Response) {
|
||||
const cmd = (req.query.cmd as string) || '';
|
||||
exec(cmd, (err, stdout) => {
|
||||
if (err) {
|
||||
res.status(500).send(String(err));
|
||||
return;
|
||||
}
|
||||
res.send(stdout);
|
||||
});
|
||||
}
|
||||
|
||||
app.get('/run', runCmd);
|
||||
|
||||
export { app, runCmd };
|
||||
25
tests/dynamic_fixtures/ts_frameworks/fastify/benign.ts
Normal file
25
tests/dynamic_fixtures/ts_frameworks/fastify/benign.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Phase 13 (Track L.11) — Fastify CMDI benign fixture (TypeScript).
|
||||
|
||||
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { execFile } from 'child_process';
|
||||
|
||||
const app = Fastify();
|
||||
const ALLOW = new Set(['status', 'uptime', 'version']);
|
||||
|
||||
async function runCmd(request: FastifyRequest, reply: FastifyReply): Promise<void> {
|
||||
const cmd = ((request.query as Record<string, string>).cmd) || '';
|
||||
if (!ALLOW.has(cmd)) {
|
||||
reply.code(400).send('rejected');
|
||||
return;
|
||||
}
|
||||
const out = await new Promise<string>((resolve) => {
|
||||
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
|
||||
resolve(err ? String(err) : stdout);
|
||||
});
|
||||
});
|
||||
reply.send(out);
|
||||
}
|
||||
|
||||
app.get('/run', runCmd);
|
||||
|
||||
export { app, runCmd };
|
||||
18
tests/dynamic_fixtures/ts_frameworks/fastify/vuln.ts
Normal file
18
tests/dynamic_fixtures/ts_frameworks/fastify/vuln.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Phase 13 (Track L.11) — Fastify CMDI vuln fixture (TypeScript).
|
||||
|
||||
import Fastify, { FastifyRequest, FastifyReply } from 'fastify';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
const app = Fastify();
|
||||
|
||||
async function runCmd(request: FastifyRequest, reply: FastifyReply): Promise<void> {
|
||||
const cmd = ((request.query as Record<string, string>).cmd) || '';
|
||||
const out = await new Promise<string>((resolve) => {
|
||||
exec(cmd, (err, stdout) => resolve(err ? String(err) : stdout));
|
||||
});
|
||||
reply.send(out);
|
||||
}
|
||||
|
||||
app.get('/run', runCmd);
|
||||
|
||||
export { app, runCmd };
|
||||
29
tests/dynamic_fixtures/ts_frameworks/koa/benign.ts
Normal file
29
tests/dynamic_fixtures/ts_frameworks/koa/benign.ts
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Phase 13 (Track L.11) — Koa CMDI benign fixture (TypeScript).
|
||||
|
||||
import Koa from 'koa';
|
||||
import Router from '@koa/router';
|
||||
import { execFile } from 'child_process';
|
||||
|
||||
const app = new Koa();
|
||||
const router = new Router();
|
||||
const ALLOW = new Set(['status', 'uptime', 'version']);
|
||||
|
||||
async function runCmd(ctx: Koa.Context): Promise<void> {
|
||||
const cmd = (ctx.query.cmd as string) || '';
|
||||
if (!ALLOW.has(cmd)) {
|
||||
ctx.status = 400;
|
||||
ctx.body = 'rejected';
|
||||
return;
|
||||
}
|
||||
await new Promise<void>((resolve) => {
|
||||
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
|
||||
ctx.body = err ? String(err) : stdout;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
router.get('/run', runCmd);
|
||||
app.use(router.routes());
|
||||
|
||||
export { app, runCmd };
|
||||
23
tests/dynamic_fixtures/ts_frameworks/koa/vuln.ts
Normal file
23
tests/dynamic_fixtures/ts_frameworks/koa/vuln.ts
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Phase 13 (Track L.11) — Koa CMDI vuln fixture (TypeScript).
|
||||
|
||||
import Koa from 'koa';
|
||||
import Router from '@koa/router';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
const app = new Koa();
|
||||
const router = new Router();
|
||||
|
||||
async function runCmd(ctx: Koa.Context): Promise<void> {
|
||||
const cmd = (ctx.query.cmd as string) || '';
|
||||
await new Promise<void>((resolve) => {
|
||||
exec(cmd, (err, stdout) => {
|
||||
ctx.body = err ? String(err) : stdout;
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
router.get('/run', runCmd);
|
||||
app.use(router.routes());
|
||||
|
||||
export { app, runCmd };
|
||||
22
tests/dynamic_fixtures/ts_frameworks/nest/benign.ts
Normal file
22
tests/dynamic_fixtures/ts_frameworks/nest/benign.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
// Phase 13 (Track L.11) — NestJS CMDI benign fixture (TypeScript).
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { execFile } from 'child_process';
|
||||
|
||||
const ALLOW = new Set(['status', 'uptime', 'version']);
|
||||
|
||||
@Controller('')
|
||||
export class AppController {
|
||||
@Get('run')
|
||||
runCmd(@Query('cmd') cmd: string): Promise<string> | string {
|
||||
if (!ALLOW.has(cmd || '')) {
|
||||
return 'rejected';
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
execFile('/usr/bin/echo', [cmd], (err, stdout) => {
|
||||
resolve(err ? String(err) : stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
20
tests/dynamic_fixtures/ts_frameworks/nest/vuln.ts
Normal file
20
tests/dynamic_fixtures/ts_frameworks/nest/vuln.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
// Phase 13 (Track L.11) — NestJS CMDI vuln fixture (TypeScript).
|
||||
//
|
||||
// Adapter binding: `@Controller('')` + `@Get('run')` on
|
||||
// `AppController.runCmd` with `cmd` flowing through `@Query('cmd')`.
|
||||
|
||||
import 'reflect-metadata';
|
||||
import { Controller, Get, Query } from '@nestjs/common';
|
||||
import { exec } from 'child_process';
|
||||
|
||||
@Controller('')
|
||||
export class AppController {
|
||||
@Get('run')
|
||||
runCmd(@Query('cmd') cmd: string): Promise<string> {
|
||||
return new Promise((resolve) => {
|
||||
exec(cmd || '', (err, stdout) => {
|
||||
resolve(err ? String(err) : stdout);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
182
tests/js_frameworks_corpus.rs
Normal file
182
tests/js_frameworks_corpus.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
//! Phase 13 (Track L.11) — JS framework adapter integration tests.
|
||||
//!
|
||||
//! Each test exercises `detect_binding` end-to-end against a fixture
|
||||
//! file under `tests/dynamic_fixtures/js_frameworks/`, asserting that
|
||||
//! the right adapter fires, the binding carries
|
||||
//! `EntryKind::HttpRoute`, and the `RouteShape` + per-formal
|
||||
//! `request_params` match the brief's contract. Benign fixtures must
|
||||
//! produce the same adapter binding shape as the vuln fixtures — the
|
||||
//! adapter only models the route, the differential outcome of a
|
||||
//! verifier run is what distinguishes the two.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::framework::{detect_binding, HttpMethod, ParamSource};
|
||||
use nyx_scanner::evidence::EntryKind;
|
||||
use nyx_scanner::summary::FuncSummary;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn parse_js(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang = tree_sitter::Language::from(tree_sitter_javascript::LANGUAGE);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
fn summary_for(name: &str, file: &str) -> FuncSummary {
|
||||
FuncSummary {
|
||||
name: name.into(),
|
||||
file_path: file.into(),
|
||||
lang: "javascript".into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn express_vuln_fixture_binds_route() {
|
||||
let path = "tests/dynamic_fixtures/js_frameworks/express/vuln.js";
|
||||
let bytes = std::fs::read(path).expect("express vuln fixture exists");
|
||||
let tree = parse_js(&bytes);
|
||||
let summary = summary_for("runCmd", path);
|
||||
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
|
||||
.expect("express adapter must bind");
|
||||
assert_eq!(binding.adapter, "js-express");
|
||||
assert_eq!(binding.kind, EntryKind::HttpRoute);
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/run");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert!(binding
|
||||
.request_params
|
||||
.iter()
|
||||
.any(|p| p.name == "req" && matches!(p.source, ParamSource::Implicit)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn express_benign_fixture_binds_same_route_shape() {
|
||||
let path = "tests/dynamic_fixtures/js_frameworks/express/benign.js";
|
||||
let bytes = std::fs::read(path).expect("express benign fixture exists");
|
||||
let tree = parse_js(&bytes);
|
||||
let summary = summary_for("runCmd", path);
|
||||
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
|
||||
.expect("express adapter must bind benign fixture");
|
||||
assert_eq!(binding.adapter, "js-express");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/run");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn koa_vuln_fixture_binds_router_route() {
|
||||
let path = "tests/dynamic_fixtures/js_frameworks/koa/vuln.js";
|
||||
let bytes = std::fs::read(path).expect("koa vuln fixture exists");
|
||||
let tree = parse_js(&bytes);
|
||||
let summary = summary_for("runCmd", path);
|
||||
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
|
||||
.expect("koa adapter must bind");
|
||||
assert_eq!(binding.adapter, "js-koa");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/run");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert!(binding
|
||||
.request_params
|
||||
.iter()
|
||||
.any(|p| p.name == "ctx" && matches!(p.source, ParamSource::Implicit)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn koa_benign_fixture_binds_same_route_shape() {
|
||||
let path = "tests/dynamic_fixtures/js_frameworks/koa/benign.js";
|
||||
let bytes = std::fs::read(path).expect("koa benign fixture exists");
|
||||
let tree = parse_js(&bytes);
|
||||
let summary = summary_for("runCmd", path);
|
||||
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
|
||||
.expect("koa adapter must bind benign fixture");
|
||||
assert_eq!(binding.adapter, "js-koa");
|
||||
assert_eq!(binding.route.as_ref().unwrap().path, "/run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fastify_vuln_fixture_binds_route() {
|
||||
let path = "tests/dynamic_fixtures/js_frameworks/fastify/vuln.js";
|
||||
let bytes = std::fs::read(path).expect("fastify vuln fixture exists");
|
||||
let tree = parse_js(&bytes);
|
||||
let summary = summary_for("runCmd", path);
|
||||
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
|
||||
.expect("fastify adapter must bind");
|
||||
assert_eq!(binding.adapter, "js-fastify");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/run");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
assert!(binding
|
||||
.request_params
|
||||
.iter()
|
||||
.any(|p| p.name == "request" && matches!(p.source, ParamSource::Implicit)));
|
||||
assert!(binding
|
||||
.request_params
|
||||
.iter()
|
||||
.any(|p| p.name == "reply" && matches!(p.source, ParamSource::Implicit)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fastify_benign_fixture_binds_same_route_shape() {
|
||||
let path = "tests/dynamic_fixtures/js_frameworks/fastify/benign.js";
|
||||
let bytes = std::fs::read(path).expect("fastify benign fixture exists");
|
||||
let tree = parse_js(&bytes);
|
||||
let summary = summary_for("runCmd", path);
|
||||
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
|
||||
.expect("fastify adapter must bind benign fixture");
|
||||
assert_eq!(binding.adapter, "js-fastify");
|
||||
assert_eq!(binding.route.as_ref().unwrap().path, "/run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nest_vuln_fixture_binds_controller_route() {
|
||||
let path = "tests/dynamic_fixtures/js_frameworks/nest/vuln.js";
|
||||
let bytes = std::fs::read(path).expect("nest vuln fixture exists");
|
||||
let tree = parse_js(&bytes);
|
||||
let summary = summary_for("runCmd", path);
|
||||
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
|
||||
.expect("nest adapter must bind");
|
||||
assert_eq!(binding.adapter, "js-nest");
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/run");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
let cmd_binding = binding
|
||||
.request_params
|
||||
.iter()
|
||||
.find(|p| p.name == "cmd")
|
||||
.expect("cmd formal");
|
||||
match &cmd_binding.source {
|
||||
ParamSource::QueryParam(q) => assert_eq!(q, "cmd"),
|
||||
other => panic!("expected QueryParam(\"cmd\"), got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nest_benign_fixture_binds_same_route_shape() {
|
||||
let path = "tests/dynamic_fixtures/js_frameworks/nest/benign.js";
|
||||
let bytes = std::fs::read(path).expect("nest benign fixture exists");
|
||||
let tree = parse_js(&bytes);
|
||||
let summary = summary_for("runCmd", path);
|
||||
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::JavaScript)
|
||||
.expect("nest adapter must bind benign fixture");
|
||||
assert_eq!(binding.adapter, "js-nest");
|
||||
assert_eq!(binding.route.as_ref().unwrap().path, "/run");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn express_adapter_runs_before_fastify_for_express_files() {
|
||||
// Regression guard: an Express file does not pull in `fastify`,
|
||||
// so the Fastify adapter never fires. Registration order is
|
||||
// alphabetical (`js-express` before `js-fastify`) which keeps the
|
||||
// adapter dispatch deterministic.
|
||||
let src: &[u8] = b"const express = require('express');\n\
|
||||
const app = express();\n\
|
||||
function h(req, res) { res.send('ok'); }\n\
|
||||
app.get('/x', h);\n";
|
||||
let tree = parse_js(src);
|
||||
let summary = summary_for("h", "synthetic.js");
|
||||
let binding =
|
||||
detect_binding(&summary, tree.root_node(), src, Lang::JavaScript).expect("fires");
|
||||
assert_eq!(binding.adapter, "js-express");
|
||||
}
|
||||
68
tests/ts_frameworks_corpus.rs
Normal file
68
tests/ts_frameworks_corpus.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
//! Phase 13 (Track L.11) — TypeScript framework adapter integration tests.
|
||||
//!
|
||||
//! Mirrors `tests/js_frameworks_corpus.rs` against the TS fixtures.
|
||||
//! The Express / Koa / Fastify adapters are registered under
|
||||
//! [`Lang::JavaScript`] only (TypeScript code paths share the JS
|
||||
//! adapter via the Lang dispatch); the Nest adapter is registered
|
||||
//! under both [`Lang::JavaScript`] and [`Lang::TypeScript`] because
|
||||
//! Nest is TypeScript-first.
|
||||
|
||||
#![cfg(feature = "dynamic")]
|
||||
|
||||
use nyx_scanner::dynamic::framework::{detect_binding, HttpMethod, ParamSource};
|
||||
use nyx_scanner::evidence::EntryKind;
|
||||
use nyx_scanner::summary::FuncSummary;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn parse_ts(src: &[u8]) -> tree_sitter::Tree {
|
||||
let mut parser = tree_sitter::Parser::new();
|
||||
let lang =
|
||||
tree_sitter::Language::from(tree_sitter_typescript::LANGUAGE_TYPESCRIPT);
|
||||
parser.set_language(&lang).unwrap();
|
||||
parser.parse(src, None).unwrap()
|
||||
}
|
||||
|
||||
fn summary_for(name: &str, file: &str) -> FuncSummary {
|
||||
FuncSummary {
|
||||
name: name.into(),
|
||||
file_path: file.into(),
|
||||
lang: "typescript".into(),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nest_ts_vuln_fixture_binds_controller_route() {
|
||||
let path = "tests/dynamic_fixtures/ts_frameworks/nest/vuln.ts";
|
||||
let bytes = std::fs::read(path).expect("nest TS vuln fixture exists");
|
||||
let tree = parse_ts(&bytes);
|
||||
let summary = summary_for("runCmd", path);
|
||||
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::TypeScript)
|
||||
.expect("ts-nest adapter must bind");
|
||||
assert_eq!(binding.adapter, "ts-nest");
|
||||
assert_eq!(binding.kind, EntryKind::HttpRoute);
|
||||
let route = binding.route.as_ref().expect("route");
|
||||
assert_eq!(route.path, "/run");
|
||||
assert_eq!(route.method, HttpMethod::GET);
|
||||
let cmd_binding = binding
|
||||
.request_params
|
||||
.iter()
|
||||
.find(|p| p.name == "cmd")
|
||||
.expect("cmd formal");
|
||||
match &cmd_binding.source {
|
||||
ParamSource::QueryParam(q) => assert_eq!(q, "cmd"),
|
||||
other => panic!("expected QueryParam, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nest_ts_benign_fixture_binds_same_route_shape() {
|
||||
let path = "tests/dynamic_fixtures/ts_frameworks/nest/benign.ts";
|
||||
let bytes = std::fs::read(path).expect("nest TS benign fixture exists");
|
||||
let tree = parse_ts(&bytes);
|
||||
let summary = summary_for("runCmd", path);
|
||||
let binding = detect_binding(&summary, tree.root_node(), &bytes, Lang::TypeScript)
|
||||
.expect("ts-nest adapter must bind benign fixture");
|
||||
assert_eq!(binding.adapter, "ts-nest");
|
||||
assert_eq!(binding.route.as_ref().unwrap().path, "/run");
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue