mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-30 20:39:39 +02:00
[pitboss] phase 15: Track B — Go + PHP + Ruby harness emitter shapes
This commit is contained in:
parent
919bc4e7e2
commit
a9b61a9126
39 changed files with 2142 additions and 186 deletions
18
tests/dynamic_fixtures/go/flag_cli/benign.go
Normal file
18
tests/dynamic_fixtures/go/flag_cli/benign.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Phase 15 — flag.Parse CLI, benign.
|
||||
// Echoes a fixed string; argv is discarded.
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Run() {
|
||||
flag.Parse()
|
||||
_ = flag.Args()
|
||||
cmd := exec.Command("echo", "hello")
|
||||
out, _ := cmd.CombinedOutput()
|
||||
fmt.Print(string(out))
|
||||
}
|
||||
3
tests/dynamic_fixtures/go/flag_cli/go.mod
Normal file
3
tests/dynamic_fixtures/go/flag_cli/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module nyx_flag_cli_fixture
|
||||
|
||||
go 1.21
|
||||
23
tests/dynamic_fixtures/go/flag_cli/vuln.go
Normal file
23
tests/dynamic_fixtures/go/flag_cli/vuln.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Phase 15 — flag.Parse CLI, vulnerable.
|
||||
// Reads the first non-flag argv positional and pipes to /bin/sh -c.
|
||||
// Entry: Run() Cap: CODE_EXEC
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Run() {
|
||||
fmt.Print("__NYX_SINK_HIT__\n")
|
||||
flag.Parse()
|
||||
payload := ""
|
||||
if flag.NArg() > 0 {
|
||||
payload = flag.Arg(0)
|
||||
}
|
||||
cmd := exec.Command("sh", "-c", "echo hello "+payload)
|
||||
out, _ := cmd.CombinedOutput()
|
||||
fmt.Print(string(out))
|
||||
}
|
||||
19
tests/dynamic_fixtures/go/fuzz_variadic/benign.go
Normal file
19
tests/dynamic_fixtures/go/fuzz_variadic/benign.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Phase 15 — fuzz-style variadic harness, benign.
|
||||
// Validates input length then echoes a fixed string.
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func FuzzHandle(data []byte) error {
|
||||
if len(data) > 1024 {
|
||||
return fmt.Errorf("too long")
|
||||
}
|
||||
cmd := exec.Command("echo", "hello")
|
||||
out, _ := cmd.CombinedOutput()
|
||||
fmt.Print(string(out))
|
||||
return nil
|
||||
}
|
||||
3
tests/dynamic_fixtures/go/fuzz_variadic/go.mod
Normal file
3
tests/dynamic_fixtures/go/fuzz_variadic/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module nyx_fuzz_variadic_fixture
|
||||
|
||||
go 1.21
|
||||
18
tests/dynamic_fixtures/go/fuzz_variadic/vuln.go
Normal file
18
tests/dynamic_fixtures/go/fuzz_variadic/vuln.go
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
// Phase 15 — fuzz-style variadic harness, vulnerable.
|
||||
// Takes raw bytes and pipes to /bin/sh -c.
|
||||
// Entry: FuzzHandle(data []byte) error Cap: CODE_EXEC
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func FuzzHandle(data []byte) error {
|
||||
fmt.Print("__NYX_SINK_HIT__\n")
|
||||
cmd := exec.Command("sh", "-c", "echo hello "+string(data))
|
||||
out, _ := cmd.CombinedOutput()
|
||||
fmt.Print(string(out))
|
||||
return nil
|
||||
}
|
||||
19
tests/dynamic_fixtures/go/gin_handler/benign.go
Normal file
19
tests/dynamic_fixtures/go/gin_handler/benign.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Phase 15 — gin handler, benign.
|
||||
// Echoes a fixed string; query value is discarded.
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"nyx-harness/entry/gin"
|
||||
)
|
||||
|
||||
func Handle(c *gin.Context) {
|
||||
_ = c.Query("payload")
|
||||
cmd := exec.Command("echo", "hello")
|
||||
out, _ := cmd.CombinedOutput()
|
||||
fmt.Print(string(out))
|
||||
c.String(200, "%s", string(out))
|
||||
}
|
||||
3
tests/dynamic_fixtures/go/gin_handler/go.mod
Normal file
3
tests/dynamic_fixtures/go/gin_handler/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module nyx_gin_handler_fixture
|
||||
|
||||
go 1.21
|
||||
21
tests/dynamic_fixtures/go/gin_handler/vuln.go
Normal file
21
tests/dynamic_fixtures/go/gin_handler/vuln.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Phase 15 — gin handler, vulnerable.
|
||||
// Reads gin context query value and pipes to /bin/sh -c.
|
||||
// Entry: Handle(c *gin.Context) Cap: CODE_EXEC
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
|
||||
"nyx-harness/entry/gin"
|
||||
)
|
||||
|
||||
func Handle(c *gin.Context) {
|
||||
fmt.Print("__NYX_SINK_HIT__\n")
|
||||
payload := c.Query("payload")
|
||||
cmd := exec.Command("sh", "-c", "echo hello "+payload)
|
||||
out, _ := cmd.CombinedOutput()
|
||||
fmt.Print(string(out))
|
||||
c.String(200, "%s", string(out))
|
||||
}
|
||||
19
tests/dynamic_fixtures/go/handler_func/benign.go
Normal file
19
tests/dynamic_fixtures/go/handler_func/benign.go
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
// Phase 15 — http.HandlerFunc, benign.
|
||||
// Echoes a fixed string; query value is discarded.
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Handle(w http.ResponseWriter, r *http.Request) {
|
||||
_ = r.URL.Query().Get("payload")
|
||||
cmd := exec.Command("echo", "hello")
|
||||
out, _ := cmd.CombinedOutput()
|
||||
fmt.Print(string(out))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(out)
|
||||
}
|
||||
3
tests/dynamic_fixtures/go/handler_func/go.mod
Normal file
3
tests/dynamic_fixtures/go/handler_func/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
module nyx_handler_func_fixture
|
||||
|
||||
go 1.21
|
||||
21
tests/dynamic_fixtures/go/handler_func/vuln.go
Normal file
21
tests/dynamic_fixtures/go/handler_func/vuln.go
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
// Phase 15 — http.HandlerFunc, vulnerable.
|
||||
// Reads `?payload=` query value and pipes to /bin/sh -c.
|
||||
// Entry: Handle(w http.ResponseWriter, r *http.Request) Cap: CODE_EXEC
|
||||
|
||||
package entry
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os/exec"
|
||||
)
|
||||
|
||||
func Handle(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Print("__NYX_SINK_HIT__\n")
|
||||
payload := r.URL.Query().Get("payload")
|
||||
cmd := exec.Command("sh", "-c", "echo hello "+payload)
|
||||
out, _ := cmd.CombinedOutput()
|
||||
fmt.Print(string(out))
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write(out)
|
||||
}
|
||||
11
tests/dynamic_fixtures/php/cli_script/benign.php
Normal file
11
tests/dynamic_fixtures/php/cli_script/benign.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
// Phase 15 — CLI script with $argv, benign.
|
||||
// Validates $argv[1] then runs a fixed echo.
|
||||
|
||||
$payload = $argv[1] ?? '';
|
||||
if (!preg_match('/^[A-Za-z0-9]{1,32}$/', $payload)) {
|
||||
echo "invalid\n";
|
||||
exit(0);
|
||||
}
|
||||
$out = shell_exec("echo hello");
|
||||
echo $out;
|
||||
6
tests/dynamic_fixtures/php/cli_script/composer.json
Normal file
6
tests/dynamic_fixtures/php/cli_script/composer.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "nyx/cli-script-fixture",
|
||||
"require": {
|
||||
"php": ">=8.0"
|
||||
}
|
||||
}
|
||||
9
tests/dynamic_fixtures/php/cli_script/vuln.php
Normal file
9
tests/dynamic_fixtures/php/cli_script/vuln.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
// Phase 15 — CLI script with $argv, vulnerable.
|
||||
// Top-level body reads $argv[1] and pipes to /bin/sh -c.
|
||||
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
|
||||
$payload = $argv[1] ?? '';
|
||||
$out = shell_exec("echo hello " . $payload);
|
||||
echo $out;
|
||||
17
tests/dynamic_fixtures/php/route_closure/benign.php
Normal file
17
tests/dynamic_fixtures/php/route_closure/benign.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
// Phase 15 — Slim/Laravel-style route closure, benign.
|
||||
// Validates payload before invoking sink.
|
||||
|
||||
$GLOBALS['__nyx_route'] = function ($payload) {
|
||||
if (!preg_match('/^[A-Za-z0-9]{1,32}$/', (string)$payload)) {
|
||||
echo "invalid\n";
|
||||
return "invalid";
|
||||
}
|
||||
$out = shell_exec("echo hello");
|
||||
echo $out;
|
||||
return $out;
|
||||
};
|
||||
|
||||
if (false) {
|
||||
$app->get('/run', $GLOBALS['__nyx_route']);
|
||||
}
|
||||
6
tests/dynamic_fixtures/php/route_closure/composer.json
Normal file
6
tests/dynamic_fixtures/php/route_closure/composer.json
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "nyx/route-closure-fixture",
|
||||
"require": {
|
||||
"php": ">=8.0"
|
||||
}
|
||||
}
|
||||
17
tests/dynamic_fixtures/php/route_closure/vuln.php
Normal file
17
tests/dynamic_fixtures/php/route_closure/vuln.php
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?php
|
||||
// Phase 15 — Slim/Laravel-style route closure, vulnerable.
|
||||
// Reads payload and pipes to /bin/sh -c.
|
||||
// Entry: route closure Cap: CODE_EXEC
|
||||
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
|
||||
$GLOBALS['__nyx_route'] = function ($payload) {
|
||||
$out = shell_exec("echo hello " . $payload);
|
||||
echo $out;
|
||||
return $out;
|
||||
};
|
||||
|
||||
// Slim-shape marker so PhpShape::detect picks RouteClosure.
|
||||
if (false) {
|
||||
$app->get('/run', $GLOBALS['__nyx_route']);
|
||||
}
|
||||
11
tests/dynamic_fixtures/php/top_level_script/benign.php
Normal file
11
tests/dynamic_fixtures/php/top_level_script/benign.php
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<?php
|
||||
// Phase 15 — top-level script (no function entry), benign.
|
||||
// Validates payload before invoking sink.
|
||||
|
||||
$payload = getenv('NYX_PAYLOAD') ?: '';
|
||||
if (!preg_match('/^[A-Za-z0-9]{1,32}$/', $payload)) {
|
||||
echo "invalid\n";
|
||||
exit(0);
|
||||
}
|
||||
$out = shell_exec("echo hello");
|
||||
echo $out;
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"name": "nyx/top-level-script-fixture",
|
||||
"require": {
|
||||
"php": ">=8.0"
|
||||
}
|
||||
}
|
||||
9
tests/dynamic_fixtures/php/top_level_script/vuln.php
Normal file
9
tests/dynamic_fixtures/php/top_level_script/vuln.php
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<?php
|
||||
// Phase 15 — top-level script (no function entry), vulnerable.
|
||||
// Body reads NYX_PAYLOAD env var directly and pipes to /bin/sh -c.
|
||||
|
||||
echo "__NYX_SINK_HIT__\n";
|
||||
|
||||
$payload = getenv('NYX_PAYLOAD') ?: '';
|
||||
$out = shell_exec("echo hello " . $payload);
|
||||
echo $out;
|
||||
4
tests/dynamic_fixtures/ruby/controller_method/Gemfile
Normal file
4
tests/dynamic_fixtures/ruby/controller_method/Gemfile
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
# Phase 15 fixture — generic controller-method shape. No framework
|
||||
# dep is required at runtime; the Gemfile is informational.
|
||||
13
tests/dynamic_fixtures/ruby/controller_method/benign.rb
Normal file
13
tests/dynamic_fixtures/ruby/controller_method/benign.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Phase 15 — generic instance method on a controller, benign.
|
||||
|
||||
class LoginController
|
||||
def authenticate(payload)
|
||||
unless payload =~ /\A[A-Za-z0-9]{1,32}\z/
|
||||
STDOUT.print("invalid\n")
|
||||
return "invalid"
|
||||
end
|
||||
out = `echo hello`
|
||||
STDOUT.print(out)
|
||||
out
|
||||
end
|
||||
end
|
||||
12
tests/dynamic_fixtures/ruby/controller_method/vuln.rb
Normal file
12
tests/dynamic_fixtures/ruby/controller_method/vuln.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
# Phase 15 — generic instance method on a controller, vulnerable.
|
||||
# No framework markers — RubyShape::detect picks ControllerMethod
|
||||
# from the class+def pair.
|
||||
|
||||
class LoginController
|
||||
def authenticate(payload)
|
||||
STDOUT.print("__NYX_SINK_HIT__\n")
|
||||
out = `echo hello #{payload}`
|
||||
STDOUT.print(out)
|
||||
out
|
||||
end
|
||||
end
|
||||
6
tests/dynamic_fixtures/ruby/rack_middleware/Gemfile
Normal file
6
tests/dynamic_fixtures/ruby/rack_middleware/Gemfile
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
# Phase 15 fixture — Rack middleware shape. The harness constructs
|
||||
# a Rack-shaped env hash and dispatches; the rack gem is not required
|
||||
# at runtime because the env-hash invocation pattern is standalone.
|
||||
gem 'rack'
|
||||
16
tests/dynamic_fixtures/ruby/rack_middleware/benign.rb
Normal file
16
tests/dynamic_fixtures/ruby/rack_middleware/benign.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
# Phase 15 — Rack middleware, benign.
|
||||
|
||||
class NyxRackApp
|
||||
def initialize(app = nil); @app = app; end
|
||||
|
||||
def call(env)
|
||||
payload = env['nyx.payload'] || ENV['NYX_PAYLOAD'] || ''
|
||||
unless payload =~ /\A[A-Za-z0-9]{1,32}\z/
|
||||
[400, { 'Content-Type' => 'text/plain' }, ['invalid']]
|
||||
else
|
||||
out = `echo hello`
|
||||
STDOUT.print(out)
|
||||
[200, { 'Content-Type' => 'text/plain' }, [out]]
|
||||
end
|
||||
end
|
||||
end
|
||||
14
tests/dynamic_fixtures/ruby/rack_middleware/vuln.rb
Normal file
14
tests/dynamic_fixtures/ruby/rack_middleware/vuln.rb
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# Phase 15 — Rack middleware, vulnerable.
|
||||
# `call(env)` reads env['nyx.payload'] and pipes to /bin/sh -c.
|
||||
|
||||
class NyxRackApp
|
||||
def initialize(app = nil); @app = app; end
|
||||
|
||||
def call(env)
|
||||
STDOUT.print("__NYX_SINK_HIT__\n")
|
||||
payload = env['nyx.payload'] || ENV['NYX_PAYLOAD'] || ''
|
||||
out = `echo hello #{payload}`
|
||||
STDOUT.print(out)
|
||||
[200, { 'Content-Type' => 'text/plain' }, [out]]
|
||||
end
|
||||
end
|
||||
7
tests/dynamic_fixtures/ruby/rails_action/Gemfile
Normal file
7
tests/dynamic_fixtures/ruby/rails_action/Gemfile
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
# Phase 15 fixture — Rails action shape. The harness instantiates
|
||||
# the controller via .new and calls the action through reflection;
|
||||
# the rails gem is not actually required at runtime. The Gemfile is
|
||||
# informational so cargo-side fixture pickup sees a non-empty manifest.
|
||||
gem 'rails'
|
||||
24
tests/dynamic_fixtures/ruby/rails_action/benign.rb
Normal file
24
tests/dynamic_fixtures/ruby/rails_action/benign.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Phase 15 — Rails-style controller action, benign.
|
||||
|
||||
class ApplicationController
|
||||
def initialize; end
|
||||
end
|
||||
|
||||
class UsersController < ApplicationController
|
||||
def initialize
|
||||
super
|
||||
@__nyx_payload = nil
|
||||
@__nyx_request = nil
|
||||
end
|
||||
|
||||
def index
|
||||
payload = @__nyx_payload || ENV['NYX_PAYLOAD'] || ''
|
||||
unless payload =~ /\A[A-Za-z0-9]{1,32}\z/
|
||||
STDOUT.print("invalid\n")
|
||||
return "invalid"
|
||||
end
|
||||
out = `echo hello`
|
||||
STDOUT.print(out)
|
||||
out
|
||||
end
|
||||
end
|
||||
23
tests/dynamic_fixtures/ruby/rails_action/vuln.rb
Normal file
23
tests/dynamic_fixtures/ruby/rails_action/vuln.rb
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
# Phase 15 — Rails-style controller action, vulnerable.
|
||||
# Controller inherits the conventional ApplicationController name so
|
||||
# RubyShape::detect picks RailsAction.
|
||||
|
||||
class ApplicationController
|
||||
def initialize; end
|
||||
end
|
||||
|
||||
class UsersController < ApplicationController
|
||||
def initialize
|
||||
super
|
||||
@__nyx_payload = nil
|
||||
@__nyx_request = nil
|
||||
end
|
||||
|
||||
def index
|
||||
STDOUT.print("__NYX_SINK_HIT__\n")
|
||||
payload = @__nyx_payload || ENV['NYX_PAYLOAD'] || ''
|
||||
out = `echo hello #{payload}`
|
||||
STDOUT.print(out)
|
||||
out
|
||||
end
|
||||
end
|
||||
6
tests/dynamic_fixtures/ruby/sinatra_route/Gemfile
Normal file
6
tests/dynamic_fixtures/ruby/sinatra_route/Gemfile
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
source 'https://rubygems.org'
|
||||
|
||||
# Phase 15 fixture — Sinatra route shape. The harness emits its own
|
||||
# route registry shim so the real sinatra gem is not required at
|
||||
# runtime; the Gemfile is informational for cargo-side fixture pickup.
|
||||
gem 'sinatra'
|
||||
13
tests/dynamic_fixtures/ruby/sinatra_route/benign.rb
Normal file
13
tests/dynamic_fixtures/ruby/sinatra_route/benign.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
# Phase 15 — Sinatra route, benign.
|
||||
# Validates payload then runs a fixed echo.
|
||||
|
||||
# nyx-shape: sinatra
|
||||
get '/run' do |payload|
|
||||
unless payload =~ /\A[A-Za-z0-9]{1,32}\z/
|
||||
STDOUT.print("invalid\n")
|
||||
next "invalid"
|
||||
end
|
||||
out = `echo hello`
|
||||
STDOUT.print(out)
|
||||
out
|
||||
end
|
||||
11
tests/dynamic_fixtures/ruby/sinatra_route/vuln.rb
Normal file
11
tests/dynamic_fixtures/ruby/sinatra_route/vuln.rb
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
# Phase 15 — Sinatra route, vulnerable.
|
||||
# Reads payload (passed by harness via block argument) and pipes through /bin/sh.
|
||||
# Entry: route block Cap: CODE_EXEC
|
||||
|
||||
# nyx-shape: sinatra
|
||||
get '/run' do |payload|
|
||||
STDOUT.print("__NYX_SINK_HIT__\n")
|
||||
out = `echo hello #{payload}`
|
||||
STDOUT.print(out)
|
||||
out
|
||||
end
|
||||
|
|
@ -9,6 +9,8 @@
|
|||
//!
|
||||
//! Run with: `cargo nextest run --features dynamic --test go_fixtures`
|
||||
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod go_fixture_tests {
|
||||
use nyx_scanner::commands::scan::Diag;
|
||||
|
|
@ -446,3 +448,175 @@ mod go_fixture_tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 15: per-shape acceptance ───────────────────────────────────────────
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod phase15_shape_tests {
|
||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||||
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn go_available() -> bool {
|
||||
std::process::Command::new("go")
|
||||
.arg("version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/vuln: expected Confirmed, got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_not_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert!(
|
||||
matches!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed | VerifyStatus::Inconclusive
|
||||
),
|
||||
"{shape}/benign: expected NotConfirmed (or Inconclusive), got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
assert_ne!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/benign: must not confirm",
|
||||
);
|
||||
}
|
||||
|
||||
fn run(
|
||||
shape: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
Lang::Go, "go", shape, file, func, cap, sink_line, kind, slot,
|
||||
)
|
||||
}
|
||||
|
||||
// ── handler_func ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn handler_func_vuln_is_confirmed() {
|
||||
if !go_available() {
|
||||
eprintln!("SKIP: go not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"handler_func", "vuln.go", "Handle", Cap::CODE_EXEC, 17,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()),
|
||||
);
|
||||
assert_confirmed("handler_func", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handler_func_benign_not_confirmed() {
|
||||
if !go_available() {
|
||||
eprintln!("SKIP: go not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"handler_func", "benign.go", "Handle", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()),
|
||||
);
|
||||
assert_not_confirmed("handler_func", &r);
|
||||
}
|
||||
|
||||
// ── gin_handler ──────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn gin_handler_vuln_is_confirmed() {
|
||||
if !go_available() {
|
||||
eprintln!("SKIP: go not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"gin_handler", "vuln.go", "Handle", Cap::CODE_EXEC, 16,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()),
|
||||
);
|
||||
assert_confirmed("gin_handler", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gin_handler_benign_not_confirmed() {
|
||||
if !go_available() {
|
||||
eprintln!("SKIP: go not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"gin_handler", "benign.go", "Handle", Cap::CODE_EXEC, 14,
|
||||
EntryKind::HttpRoute, PayloadSlot::QueryParam("payload".into()),
|
||||
);
|
||||
assert_not_confirmed("gin_handler", &r);
|
||||
}
|
||||
|
||||
// ── flag_cli ─────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn flag_cli_vuln_is_confirmed() {
|
||||
if !go_available() {
|
||||
eprintln!("SKIP: go not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"flag_cli", "vuln.go", "Run", Cap::CODE_EXEC, 19,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_confirmed("flag_cli", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flag_cli_benign_not_confirmed() {
|
||||
if !go_available() {
|
||||
eprintln!("SKIP: go not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"flag_cli", "benign.go", "Run", Cap::CODE_EXEC, 15,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_not_confirmed("flag_cli", &r);
|
||||
}
|
||||
|
||||
// ── fuzz_variadic ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn fuzz_variadic_vuln_is_confirmed() {
|
||||
if !go_available() {
|
||||
eprintln!("SKIP: go not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"fuzz_variadic", "vuln.go", "FuzzHandle", Cap::CODE_EXEC, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("fuzz_variadic", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fuzz_variadic_benign_not_confirmed() {
|
||||
if !go_available() {
|
||||
eprintln!("SKIP: go not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"fuzz_variadic", "benign.go", "FuzzHandle", Cap::CODE_EXEC, 14,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("fuzz_variadic", &r);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@
|
|||
//!
|
||||
//! Run with: `cargo nextest run --features dynamic --test php_fixtures`
|
||||
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod php_fixture_tests {
|
||||
use nyx_scanner::commands::scan::Diag;
|
||||
|
|
@ -446,3 +448,147 @@ mod php_fixture_tests {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Phase 15: per-shape acceptance ───────────────────────────────────────────
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod phase15_shape_tests {
|
||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||||
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn php_available() -> bool {
|
||||
std::process::Command::new("php")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/vuln: expected Confirmed, got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_not_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert!(
|
||||
matches!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed | VerifyStatus::Inconclusive
|
||||
),
|
||||
"{shape}/benign: expected NotConfirmed (or Inconclusive), got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
assert_ne!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/benign: must not confirm",
|
||||
);
|
||||
}
|
||||
|
||||
fn run(
|
||||
shape: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
Lang::Php, "php", shape, file, func, cap, sink_line, kind, slot,
|
||||
)
|
||||
}
|
||||
|
||||
// ── route_closure ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn route_closure_vuln_is_confirmed() {
|
||||
if !php_available() {
|
||||
eprintln!("SKIP: php not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"route_closure", "vuln.php", "run", Cap::CODE_EXEC, 10,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("route_closure", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_closure_benign_not_confirmed() {
|
||||
if !php_available() {
|
||||
eprintln!("SKIP: php not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"route_closure", "benign.php", "run", Cap::CODE_EXEC, 11,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("route_closure", &r);
|
||||
}
|
||||
|
||||
// ── cli_script ───────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn cli_script_vuln_is_confirmed() {
|
||||
if !php_available() {
|
||||
eprintln!("SKIP: php not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"cli_script", "vuln.php", "main", Cap::CODE_EXEC, 8,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_confirmed("cli_script", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_script_benign_not_confirmed() {
|
||||
if !php_available() {
|
||||
eprintln!("SKIP: php not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"cli_script", "benign.php", "main", Cap::CODE_EXEC, 11,
|
||||
EntryKind::CliSubcommand, PayloadSlot::Argv(0),
|
||||
);
|
||||
assert_not_confirmed("cli_script", &r);
|
||||
}
|
||||
|
||||
// ── top_level_script ─────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn top_level_script_vuln_is_confirmed() {
|
||||
if !php_available() {
|
||||
eprintln!("SKIP: php not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"top_level_script", "vuln.php", "", Cap::CODE_EXEC, 8,
|
||||
EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||
);
|
||||
assert_confirmed("top_level_script", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn top_level_script_benign_not_confirmed() {
|
||||
if !php_available() {
|
||||
eprintln!("SKIP: php not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"top_level_script", "benign.php", "", Cap::CODE_EXEC, 10,
|
||||
EntryKind::Function, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||
);
|
||||
assert_not_confirmed("top_level_script", &r);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
182
tests/ruby_fixtures.rs
Normal file
182
tests/ruby_fixtures.rs
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
//! Ruby fixture integration tests (Phase 15 acceptance gate).
|
||||
//!
|
||||
//! Per-shape acceptance for the Ruby emitter shapes shipped in Phase 15
|
||||
//! (Track B Ruby vertical): Sinatra route, Rails action, Rack middleware,
|
||||
//! and generic controller method. Each shape ships a `vuln.rb` + `benign.rb`
|
||||
//! pair under `tests/dynamic_fixtures/ruby/<shape>/`.
|
||||
//!
|
||||
//! Prerequisites: skips cleanly when `ruby` is unavailable on the host.
|
||||
//!
|
||||
//! Run with: `cargo nextest run --features dynamic --test ruby_fixtures`
|
||||
|
||||
mod common;
|
||||
|
||||
#[cfg(feature = "dynamic")]
|
||||
mod phase15_shape_tests {
|
||||
use crate::common::fixture_harness::run_shape_fixture_lang;
|
||||
use nyx_scanner::dynamic::spec::PayloadSlot;
|
||||
use nyx_scanner::evidence::{EntryKind, VerifyResult, VerifyStatus};
|
||||
use nyx_scanner::labels::Cap;
|
||||
use nyx_scanner::symbol::Lang;
|
||||
|
||||
fn ruby_available() -> bool {
|
||||
std::process::Command::new("ruby")
|
||||
.arg("--version")
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn assert_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert_eq!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/vuln: expected Confirmed, got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_not_confirmed(shape: &str, result: &VerifyResult) {
|
||||
assert!(
|
||||
matches!(
|
||||
result.status,
|
||||
VerifyStatus::NotConfirmed | VerifyStatus::Inconclusive
|
||||
),
|
||||
"{shape}/benign: expected NotConfirmed (or Inconclusive), got {:?} ({:?})",
|
||||
result.status,
|
||||
result.detail,
|
||||
);
|
||||
assert_ne!(
|
||||
result.status,
|
||||
VerifyStatus::Confirmed,
|
||||
"{shape}/benign: must not confirm",
|
||||
);
|
||||
}
|
||||
|
||||
fn run(
|
||||
shape: &str,
|
||||
file: &str,
|
||||
func: &str,
|
||||
cap: Cap,
|
||||
sink_line: u32,
|
||||
kind: EntryKind,
|
||||
slot: PayloadSlot,
|
||||
) -> VerifyResult {
|
||||
run_shape_fixture_lang(
|
||||
Lang::Ruby, "ruby", shape, file, func, cap, sink_line, kind, slot,
|
||||
)
|
||||
}
|
||||
|
||||
// ── sinatra_route ────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn sinatra_route_vuln_is_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"sinatra_route", "vuln.rb", "run", Cap::CODE_EXEC, 7,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("sinatra_route", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sinatra_route_benign_not_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"sinatra_route", "benign.rb", "run", Cap::CODE_EXEC, 10,
|
||||
EntryKind::HttpRoute, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("sinatra_route", &r);
|
||||
}
|
||||
|
||||
// ── rails_action ─────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn rails_action_vuln_is_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"rails_action", "vuln.rb", "index", Cap::CODE_EXEC, 17,
|
||||
EntryKind::HttpRoute, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||
);
|
||||
assert_confirmed("rails_action", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rails_action_benign_not_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"rails_action", "benign.rb", "index", Cap::CODE_EXEC, 20,
|
||||
EntryKind::HttpRoute, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||
);
|
||||
assert_not_confirmed("rails_action", &r);
|
||||
}
|
||||
|
||||
// ── rack_middleware ──────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn rack_middleware_vuln_is_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"rack_middleware", "vuln.rb", "call", Cap::CODE_EXEC, 9,
|
||||
EntryKind::HttpRoute, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||
);
|
||||
assert_confirmed("rack_middleware", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rack_middleware_benign_not_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"rack_middleware", "benign.rb", "call", Cap::CODE_EXEC, 11,
|
||||
EntryKind::HttpRoute, PayloadSlot::EnvVar("NYX_PAYLOAD".into()),
|
||||
);
|
||||
assert_not_confirmed("rack_middleware", &r);
|
||||
}
|
||||
|
||||
// ── controller_method ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn controller_method_vuln_is_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"controller_method", "vuln.rb", "authenticate", Cap::CODE_EXEC, 7,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_confirmed("controller_method", &r);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn controller_method_benign_not_confirmed() {
|
||||
if !ruby_available() {
|
||||
eprintln!("SKIP: ruby not available");
|
||||
return;
|
||||
}
|
||||
let r = run(
|
||||
"controller_method", "benign.rb", "authenticate", Cap::CODE_EXEC, 10,
|
||||
EntryKind::Function, PayloadSlot::Param(0),
|
||||
);
|
||||
assert_not_confirmed("controller_method", &r);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue