mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-09 19:45:13 +02:00
[pitboss] phase 03: Track A.3 — LangEmitter::entry_kinds_supported + actionable Inconclusive hints
This commit is contained in:
parent
8211d4fd47
commit
364d09d6a8
16 changed files with 830 additions and 35 deletions
52
src/dynamic/lang/c.rs
Normal file
52
src/dynamic/lang/c.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
//! C harness emitter (stub).
|
||||
//!
|
||||
//! No harness source is generated yet — `emit` returns
|
||||
//! [`UnsupportedReason::LangUnsupported`]. The module exists so that
|
||||
//! [`crate::dynamic::lang::entry_kinds_supported`] can advertise the entry
|
||||
//! kinds Track B will deliver (Phase 16: `main(argc, argv)`,
|
||||
//! `LLVMFuzzerTestOneInput`, free functions with `(const char*, size_t)` or
|
||||
//! `(int, char**)` shapes) and so the verifier can surface
|
||||
//! `Inconclusive(EntryKindUnsupported { … })` instead of dropping C findings.
|
||||
|
||||
use crate::dynamic::lang::{HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for C.
|
||||
pub struct CEmitter;
|
||||
|
||||
/// Entry kinds the C emitter intends to support once Phase 16 lands.
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
|
||||
impl LangEmitter for CEmitter {
|
||||
fn emit(&self, _spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
Err(UnsupportedReason::LangUnsupported)
|
||||
}
|
||||
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
|
||||
SUPPORTED
|
||||
}
|
||||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"c emitter is a stub; once Phase 16 (Track B Rust + C/C++ vertical) lands it will support {SUPPORTED:?} plus libFuzzer + main(argc, argv) shapes — attempted `EntryKind::{attempted}`"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn entry_kinds_supported_is_non_empty() {
|
||||
assert!(!CEmitter.entry_kinds_supported().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kind_hint_names_attempted_and_phase() {
|
||||
let hint = CEmitter.entry_kind_hint(EntryKind::LibraryApi);
|
||||
assert!(hint.contains("LibraryApi"));
|
||||
assert!(hint.contains("Phase 16"));
|
||||
}
|
||||
}
|
||||
52
src/dynamic/lang/cpp.rs
Normal file
52
src/dynamic/lang/cpp.rs
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
//! C++ harness emitter (stub).
|
||||
//!
|
||||
//! No harness source is generated yet — `emit` returns
|
||||
//! [`UnsupportedReason::LangUnsupported`]. The module exists so that
|
||||
//! [`crate::dynamic::lang::entry_kinds_supported`] can advertise the entry
|
||||
//! kinds Track B will deliver (Phase 16: `main(argc, argv)`,
|
||||
//! `LLVMFuzzerTestOneInput`, free functions with `(const char*, size_t)`)
|
||||
//! and so the verifier can surface `Inconclusive(EntryKindUnsupported { … })`
|
||||
//! instead of dropping C++ findings.
|
||||
|
||||
use crate::dynamic::lang::{HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for C++.
|
||||
pub struct CppEmitter;
|
||||
|
||||
/// Entry kinds the C++ emitter intends to support once Phase 16 lands.
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
|
||||
impl LangEmitter for CppEmitter {
|
||||
fn emit(&self, _spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
Err(UnsupportedReason::LangUnsupported)
|
||||
}
|
||||
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
|
||||
SUPPORTED
|
||||
}
|
||||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"cpp emitter is a stub; once Phase 16 (Track B Rust + C/C++ vertical) lands it will support {SUPPORTED:?} plus libFuzzer + main(argc, argv) shapes — attempted `EntryKind::{attempted}`"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn entry_kinds_supported_is_non_empty() {
|
||||
assert!(!CppEmitter.entry_kinds_supported().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kind_hint_names_attempted_and_phase() {
|
||||
let hint = CppEmitter.entry_kind_hint(EntryKind::CliSubcommand);
|
||||
assert!(hint.contains("CliSubcommand"));
|
||||
assert!(hint.contains("Phase 16"));
|
||||
}
|
||||
}
|
||||
|
|
@ -24,10 +24,35 @@
|
|||
//!
|
||||
//! Build container: `nyx-build-go:{toolchain_id}` (deferred; §19.1).
|
||||
|
||||
use crate::dynamic::lang::HarnessSource;
|
||||
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
|
||||
use crate::dynamic::lang::{HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for Go. Method bodies delegate to the
|
||||
/// existing free functions in this module.
|
||||
pub struct GoEmitter;
|
||||
|
||||
/// Entry kinds the Go emitter currently understands. Extended in Phase 15
|
||||
/// (Track B Go vertical) to include `HttpRoute` (`net/http`, gin) and CLI
|
||||
/// (`flag.Parse`) shapes.
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
|
||||
impl LangEmitter for GoEmitter {
|
||||
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
emit(spec)
|
||||
}
|
||||
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
|
||||
SUPPORTED
|
||||
}
|
||||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"go emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add net/http, gin, flag.Parse shapes in phase 15"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a Go harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
match &spec.payload_slot {
|
||||
|
|
@ -203,6 +228,19 @@ mod tests {
|
|||
assert_eq!(err, UnsupportedReason::EntryKindUnsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kinds_supported_is_non_empty() {
|
||||
assert!(!GoEmitter.entry_kinds_supported().is_empty());
|
||||
assert!(GoEmitter.entry_kinds_supported().contains(&EntryKind::Function));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kind_hint_names_attempted_and_phase() {
|
||||
let hint = GoEmitter.entry_kind_hint(EntryKind::HttpRoute);
|
||||
assert!(hint.contains("HttpRoute"));
|
||||
assert!(hint.contains("phase 15"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capitalize_first_handles_lowercase() {
|
||||
assert_eq!(capitalize_first("handleRequest"), "HandleRequest");
|
||||
|
|
|
|||
|
|
@ -26,10 +26,35 @@
|
|||
//!
|
||||
//! Build container: `nyx-build-java:{toolchain_id}` (deferred; §19.1).
|
||||
|
||||
use crate::dynamic::lang::HarnessSource;
|
||||
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
|
||||
use crate::dynamic::lang::{HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for Java. Method bodies delegate to the
|
||||
/// existing free functions in this module.
|
||||
pub struct JavaEmitter;
|
||||
|
||||
/// Entry kinds the Java emitter currently understands. Extended in Phase 14
|
||||
/// (Track B Java vertical) to include `HttpRoute` (servlet / Spring /
|
||||
/// Quarkus) and JUnit static-method shapes.
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
|
||||
impl LangEmitter for JavaEmitter {
|
||||
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
emit(spec)
|
||||
}
|
||||
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
|
||||
SUPPORTED
|
||||
}
|
||||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"java emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add servlet / Spring / Quarkus shapes in phase 14"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a Java harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
match &spec.payload_slot {
|
||||
|
|
@ -182,6 +207,21 @@ mod tests {
|
|||
assert_eq!(err, UnsupportedReason::EntryKindUnsupported);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kinds_supported_is_non_empty() {
|
||||
assert!(!JavaEmitter.entry_kinds_supported().is_empty());
|
||||
assert!(JavaEmitter
|
||||
.entry_kinds_supported()
|
||||
.contains(&EntryKind::Function));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kind_hint_names_attempted_and_phase() {
|
||||
let hint = JavaEmitter.entry_kind_hint(EntryKind::HttpRoute);
|
||||
assert!(hint.contains("HttpRoute"));
|
||||
assert!(hint.contains("phase 14"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harness_has_base64_decoder() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
|
|
|
|||
|
|
@ -19,10 +19,36 @@
|
|||
//! Build: no compilation step. Command is `node harness.js`.
|
||||
//! Build container: `nyx-build-node:{toolchain_id}` (deferred; §19.1).
|
||||
|
||||
use crate::dynamic::lang::HarnessSource;
|
||||
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
|
||||
use crate::dynamic::lang::{HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for JavaScript / TypeScript (one
|
||||
/// emitter, both langs share the same Node.js dispatch). Method bodies
|
||||
/// delegate to the existing free functions in this module.
|
||||
pub struct JavaScriptEmitter;
|
||||
|
||||
/// Entry kinds the JS / TS emitter currently understands. Extended in
|
||||
/// Phase 13 (Track B JS + TS vertical) to include `HttpRoute` (Express /
|
||||
/// Koa / Next), `CliSubcommand`, etc.
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
|
||||
impl LangEmitter for JavaScriptEmitter {
|
||||
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
emit(spec)
|
||||
}
|
||||
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
|
||||
SUPPORTED
|
||||
}
|
||||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"javascript / typescript emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add Express / Koa / Next shapes in phase 13"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a Node.js harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
match &spec.payload_slot {
|
||||
|
|
@ -230,6 +256,21 @@ mod tests {
|
|||
assert_eq!(harness.entry_subpath, Some("entry.js".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kinds_supported_is_non_empty() {
|
||||
assert!(!JavaScriptEmitter.entry_kinds_supported().is_empty());
|
||||
assert!(JavaScriptEmitter
|
||||
.entry_kinds_supported()
|
||||
.contains(&EntryKind::Function));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kind_hint_names_attempted_and_phase() {
|
||||
let hint = JavaScriptEmitter.entry_kind_hint(EntryKind::HttpRoute);
|
||||
assert!(hint.contains("HttpRoute"));
|
||||
assert!(hint.contains("phase 13"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_module_name_is_always_entry_to_match_copy_destination() {
|
||||
// `copy_entry_file` (via `entry_module_filename`) stages every fixture
|
||||
|
|
|
|||
|
|
@ -1,16 +1,29 @@
|
|||
//! Per-language harness emitters.
|
||||
//!
|
||||
//! Each submodule implements `emit(spec) -> HarnessSource` for one language.
|
||||
//! The top-level [`emit`] function dispatches on `spec.lang`.
|
||||
//! Each submodule implements [`LangEmitter`] for one language. The top-level
|
||||
//! [`emit`] function dispatches on `spec.lang` and validates `spec.entry_kind`
|
||||
//! against the chosen emitter's [`LangEmitter::entry_kinds_supported`] list
|
||||
//! before delegating, so unsupported entry kinds short-circuit with a typed
|
||||
//! `UnsupportedReason::EntryKindUnsupported` rather than producing a
|
||||
//! never-runnable harness.
|
||||
//!
|
||||
//! Two free helpers — [`entry_kinds_supported`] and [`entry_kind_hint`] — wrap
|
||||
//! the trait dispatch so callers outside the harness build path (notably the
|
||||
//! verifier, which surfaces an `Inconclusive` verdict with the supported list
|
||||
//! and hint baked in) can advertise capability without instantiating a spec.
|
||||
|
||||
pub mod c;
|
||||
pub mod cpp;
|
||||
pub mod go;
|
||||
pub mod java;
|
||||
pub mod javascript;
|
||||
pub mod php;
|
||||
pub mod python;
|
||||
pub mod ruby;
|
||||
pub mod rust;
|
||||
pub mod typescript;
|
||||
|
||||
use crate::dynamic::spec::HarnessSpec;
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
use crate::symbol::Lang;
|
||||
|
||||
|
|
@ -33,15 +46,128 @@ pub struct HarnessSource {
|
|||
pub entry_subpath: Option<String>,
|
||||
}
|
||||
|
||||
/// Per-language harness emitter contract.
|
||||
///
|
||||
/// Implementations are zero-sized unit structs (one per `src/dynamic/lang/*.rs`
|
||||
/// module). The [`emit`](LangEmitter::emit) method is the legacy
|
||||
/// per-language entry point retained for the build pipeline; the two
|
||||
/// capability methods are consulted both at dispatch time (`lang::emit`
|
||||
/// pre-flight check) and by the verifier when constructing
|
||||
/// `Inconclusive(EntryKindUnsupported { … })`.
|
||||
pub trait LangEmitter {
|
||||
/// Build a harness source bundle for `spec`.
|
||||
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason>;
|
||||
|
||||
/// The set of [`EntryKind`] variants this emitter understands.
|
||||
///
|
||||
/// Must be non-empty: every emitter advertises at least one shape it can
|
||||
/// (or will) drive — even stub modules whose `emit` returns
|
||||
/// `LangUnsupported`. Empty would be indistinguishable from "language
|
||||
/// not in the dispatch table" and would defeat the structured
|
||||
/// advertisement that callers consume.
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind];
|
||||
|
||||
/// Human-actionable hint produced when `attempted` is not in
|
||||
/// [`entry_kinds_supported`](LangEmitter::entry_kinds_supported).
|
||||
///
|
||||
/// The string is consumed by
|
||||
/// [`crate::evidence::InconclusiveReason::EntryKindUnsupported::hint`] and
|
||||
/// surfaces directly to operators triaging dynamic verification gaps;
|
||||
/// keep it specific (name the supported kinds, name the phase that will
|
||||
/// extend support).
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String;
|
||||
}
|
||||
|
||||
/// Dispatch to the appropriate language emitter.
|
||||
///
|
||||
/// Validates `spec.entry_kind` against the chosen emitter's supported list
|
||||
/// before delegating; an unsupported entry kind short-circuits with
|
||||
/// [`UnsupportedReason::EntryKindUnsupported`] so the verifier can surface a
|
||||
/// structured `Inconclusive` verdict with the supported list and hint baked
|
||||
/// in (instead of producing a never-runnable harness).
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
match spec.lang {
|
||||
Lang::Python => python::emit(spec),
|
||||
Lang::Rust => rust::emit(spec),
|
||||
Lang::JavaScript | Lang::TypeScript => javascript::emit(spec),
|
||||
Lang::Go => go::emit(spec),
|
||||
Lang::Java => java::emit(spec),
|
||||
Lang::Php => php::emit(spec),
|
||||
_ => Err(UnsupportedReason::LangUnsupported),
|
||||
let supported = entry_kinds_supported(spec.lang);
|
||||
if !supported.is_empty() && !supported.contains(&spec.entry_kind) {
|
||||
return Err(UnsupportedReason::EntryKindUnsupported);
|
||||
}
|
||||
dispatch(spec.lang, |e| e.emit(spec))
|
||||
.unwrap_or(Err(UnsupportedReason::LangUnsupported))
|
||||
}
|
||||
|
||||
/// Public free-fn dispatcher for the supported entry kinds of `lang`.
|
||||
///
|
||||
/// Returns an empty slice when `lang` has no registered emitter — callers
|
||||
/// distinguish that from "emitter exists but advertises none" by treating
|
||||
/// empty as "language unsupported".
|
||||
pub fn entry_kinds_supported(lang: Lang) -> &'static [EntryKind] {
|
||||
dispatch(lang, |e| e.entry_kinds_supported()).unwrap_or(&[])
|
||||
}
|
||||
|
||||
/// Public free-fn dispatcher for an emitter's hint about `attempted`.
|
||||
///
|
||||
/// Falls back to a generic message when `lang` has no registered emitter so
|
||||
/// callers do not need to special-case that path.
|
||||
pub fn entry_kind_hint(lang: Lang, attempted: EntryKind) -> String {
|
||||
dispatch(lang, |e| e.entry_kind_hint(attempted)).unwrap_or_else(|| {
|
||||
format!(
|
||||
"no harness emitter is registered for {lang:?}; attempted {attempted}"
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Internal helper: invoke `f` against the emitter registered for `lang`,
|
||||
/// returning `None` when no emitter is registered for that language.
|
||||
fn dispatch<R>(lang: Lang, f: impl FnOnce(&dyn LangEmitter) -> R) -> Option<R> {
|
||||
let emitter: Option<&dyn LangEmitter> = match lang {
|
||||
Lang::Python => Some(&python::PythonEmitter),
|
||||
Lang::Rust => Some(&rust::RustEmitter),
|
||||
Lang::JavaScript => Some(&javascript::JavaScriptEmitter),
|
||||
Lang::TypeScript => Some(&typescript::TypeScriptEmitter),
|
||||
Lang::Go => Some(&go::GoEmitter),
|
||||
Lang::Java => Some(&java::JavaEmitter),
|
||||
Lang::Php => Some(&php::PhpEmitter),
|
||||
Lang::Ruby => Some(&ruby::RubyEmitter),
|
||||
Lang::C => Some(&c::CEmitter),
|
||||
Lang::Cpp => Some(&cpp::CppEmitter),
|
||||
};
|
||||
emitter.map(f)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Every registered emitter must advertise at least one entry kind so the
|
||||
/// verifier never produces an empty `supported` list in
|
||||
/// `Inconclusive(EntryKindUnsupported { supported, .. })`.
|
||||
#[test]
|
||||
fn every_lang_advertises_at_least_one_entry_kind() {
|
||||
for lang in [
|
||||
Lang::Python,
|
||||
Lang::Rust,
|
||||
Lang::JavaScript,
|
||||
Lang::TypeScript,
|
||||
Lang::Go,
|
||||
Lang::Java,
|
||||
Lang::Php,
|
||||
Lang::Ruby,
|
||||
Lang::C,
|
||||
Lang::Cpp,
|
||||
] {
|
||||
let kinds = entry_kinds_supported(lang);
|
||||
assert!(
|
||||
!kinds.is_empty(),
|
||||
"{lang:?} emitter must advertise at least one EntryKind"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kind_hint_mentions_attempted() {
|
||||
let hint = entry_kind_hint(Lang::Python, EntryKind::HttpRoute);
|
||||
assert!(
|
||||
hint.contains("HttpRoute"),
|
||||
"hint must mention the attempted entry kind, got: {hint:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,10 +18,35 @@
|
|||
//! Build: no compilation step. Command is `php harness.php`.
|
||||
//! Build container: `nyx-build-php:{toolchain_id}` (deferred; §19.1).
|
||||
|
||||
use crate::dynamic::lang::HarnessSource;
|
||||
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
|
||||
use crate::dynamic::lang::{HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for PHP. Method bodies delegate to the
|
||||
/// existing free functions in this module.
|
||||
pub struct PhpEmitter;
|
||||
|
||||
/// Entry kinds the PHP emitter currently understands. Extended in Phase 15
|
||||
/// (Track B PHP vertical) to include `HttpRoute` (Slim / Laravel / Symfony
|
||||
/// closures) and `CliSubcommand` (`$argv`).
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
|
||||
impl LangEmitter for PhpEmitter {
|
||||
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
emit(spec)
|
||||
}
|
||||
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
|
||||
SUPPORTED
|
||||
}
|
||||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"php emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add Slim / Laravel / Symfony route + CLI shapes in phase 15"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a PHP harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
match &spec.payload_slot {
|
||||
|
|
@ -193,6 +218,21 @@ mod tests {
|
|||
assert_eq!(harness.entry_subpath, Some("entry.php".to_owned()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kinds_supported_is_non_empty() {
|
||||
assert!(!PhpEmitter.entry_kinds_supported().is_empty());
|
||||
assert!(PhpEmitter
|
||||
.entry_kinds_supported()
|
||||
.contains(&EntryKind::Function));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kind_hint_names_attempted_and_phase() {
|
||||
let hint = PhpEmitter.entry_kind_hint(EntryKind::HttpRoute);
|
||||
assert!(hint.contains("HttpRoute"));
|
||||
assert!(hint.contains("phase 15"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn harness_has_base64_decode() {
|
||||
let spec = make_spec(PayloadSlot::Param(0));
|
||||
|
|
|
|||
|
|
@ -13,10 +13,35 @@
|
|||
//! - `PayloadSlot::EnvVar(name)` — set env var before calling.
|
||||
//! - Other slots produce `UnsupportedReason::EntryKindUnsupported`.
|
||||
|
||||
use crate::dynamic::lang::HarnessSource;
|
||||
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
|
||||
use crate::dynamic::lang::{HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for Python. Registered in the
|
||||
/// `lang::dispatch` table; method bodies delegate to the existing free
|
||||
/// functions in this module.
|
||||
pub struct PythonEmitter;
|
||||
|
||||
/// Entry kinds the Python emitter currently understands. Extended in Phase 12
|
||||
/// (Track B Python vertical) to include `HttpRoute`, `CliSubcommand`, etc.
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
|
||||
impl LangEmitter for PythonEmitter {
|
||||
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
emit(spec)
|
||||
}
|
||||
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
|
||||
SUPPORTED
|
||||
}
|
||||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"python emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add framework + CLI shapes in phase 12"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a Python harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
// Validate payload slot.
|
||||
|
|
@ -237,6 +262,21 @@ mod tests {
|
|||
assert_eq!(module_name("no_ext"), "no_ext");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kinds_supported_is_non_empty() {
|
||||
assert!(!PythonEmitter.entry_kinds_supported().is_empty());
|
||||
assert!(PythonEmitter
|
||||
.entry_kinds_supported()
|
||||
.contains(&EntryKind::Function));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kind_hint_names_attempted_and_phase() {
|
||||
let hint = PythonEmitter.entry_kind_hint(EntryKind::HttpRoute);
|
||||
assert!(hint.contains("HttpRoute"));
|
||||
assert!(hint.contains("phase 12"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_lang_returns_err() {
|
||||
let mut spec = make_spec(PayloadSlot::Param(0));
|
||||
|
|
|
|||
77
src/dynamic/lang/ruby.rs
Normal file
77
src/dynamic/lang/ruby.rs
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
//! Ruby harness emitter (stub).
|
||||
//!
|
||||
//! No harness source is generated yet — `emit` returns
|
||||
//! [`UnsupportedReason::LangUnsupported`]. The module exists so that
|
||||
//! [`crate::dynamic::lang::entry_kinds_supported`] can advertise the entry
|
||||
//! kinds Track B will deliver (Phase 15: Sinatra route, Rails action, Rack
|
||||
//! middleware, generic controller method) and so the verifier can surface
|
||||
//! a structured `Inconclusive(EntryKindUnsupported { … })` instead of
|
||||
//! silently dropping Ruby findings.
|
||||
|
||||
use crate::dynamic::lang::{HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for Ruby.
|
||||
pub struct RubyEmitter;
|
||||
|
||||
/// Entry kinds the Ruby emitter intends to support once Phase 15 lands.
|
||||
/// Advertised pre-implementation so the verifier can route findings into
|
||||
/// `Inconclusive(EntryKindUnsupported)` rather than `Unsupported`.
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
|
||||
impl LangEmitter for RubyEmitter {
|
||||
fn emit(&self, _spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
Err(UnsupportedReason::LangUnsupported)
|
||||
}
|
||||
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
|
||||
SUPPORTED
|
||||
}
|
||||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"ruby emitter is a stub; once Phase 15 (Track B Ruby vertical) lands it will support {SUPPORTED:?} plus Sinatra / Rails / Rack route shapes — attempted `EntryKind::{attempted}`"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn entry_kinds_supported_is_non_empty() {
|
||||
assert!(!RubyEmitter.entry_kinds_supported().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kind_hint_names_attempted_and_phase() {
|
||||
let hint = RubyEmitter.entry_kind_hint(EntryKind::HttpRoute);
|
||||
assert!(hint.contains("HttpRoute"));
|
||||
assert!(hint.contains("Phase 15"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn emit_returns_lang_unsupported() {
|
||||
let spec = HarnessSpec {
|
||||
finding_id: "0".into(),
|
||||
entry_file: "x.rb".into(),
|
||||
entry_name: "f".into(),
|
||||
entry_kind: EntryKind::Function,
|
||||
lang: crate::symbol::Lang::Ruby,
|
||||
toolchain_id: "ruby-3".into(),
|
||||
payload_slot: crate::dynamic::spec::PayloadSlot::Param(0),
|
||||
expected_cap: crate::labels::Cap::SQL_QUERY,
|
||||
constraint_hints: vec![],
|
||||
sink_file: "x.rb".into(),
|
||||
sink_line: 1,
|
||||
spec_hash: "0".into(),
|
||||
derivation: crate::dynamic::spec::SpecDerivationStrategy::FromFlowSteps,
|
||||
};
|
||||
assert_eq!(
|
||||
RubyEmitter.emit(&spec).unwrap_err(),
|
||||
UnsupportedReason::LangUnsupported
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -21,11 +21,36 @@
|
|||
//!
|
||||
//! HTML_ESCAPE is n/a for Rust (§15.4).
|
||||
|
||||
use crate::dynamic::lang::HarnessSource;
|
||||
use crate::dynamic::spec::{HarnessSpec, PayloadSlot};
|
||||
use crate::dynamic::lang::{HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec, PayloadSlot};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
use crate::labels::Cap;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for Rust. Method bodies delegate to the
|
||||
/// existing free functions in this module.
|
||||
pub struct RustEmitter;
|
||||
|
||||
/// Entry kinds the Rust emitter currently understands. Extended in Phase 16
|
||||
/// (Track B Rust + C/C++ vertical) to include `HttpRoute` (`actix_web`,
|
||||
/// `axum`), `CliSubcommand` (clap), and `LibraryApi` (libfuzzer).
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
|
||||
impl LangEmitter for RustEmitter {
|
||||
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
emit(spec)
|
||||
}
|
||||
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
|
||||
SUPPORTED
|
||||
}
|
||||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"rust emitter supports {SUPPORTED:?}; this finding's enclosing context is `EntryKind::{attempted}` — Track B will add actix / axum / clap / libfuzzer shapes in phase 16"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit a Rust harness for `spec`.
|
||||
pub fn emit(spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
match &spec.payload_slot {
|
||||
|
|
@ -247,6 +272,21 @@ mod tests {
|
|||
assert!(cargo.contains("path = \"src/main.rs\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kinds_supported_is_non_empty() {
|
||||
assert!(!RustEmitter.entry_kinds_supported().is_empty());
|
||||
assert!(RustEmitter
|
||||
.entry_kinds_supported()
|
||||
.contains(&EntryKind::Function));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kind_hint_names_attempted_and_phase() {
|
||||
let hint = RustEmitter.entry_kind_hint(EntryKind::HttpRoute);
|
||||
assert!(hint.contains("HttpRoute"));
|
||||
assert!(hint.contains("phase 16"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn b64_decode_roundtrip() {
|
||||
// Test by compiling: actual b64_decode is in generated code.
|
||||
|
|
|
|||
64
src/dynamic/lang/typescript.rs
Normal file
64
src/dynamic/lang/typescript.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
//! TypeScript harness emitter.
|
||||
//!
|
||||
//! Today TypeScript shares the JS emitter — `tsc` is not invoked; the runner
|
||||
//! treats `.ts` / `.tsx` / `.mts` / `.cts` files as Node-compatible because
|
||||
//! every shape we currently emit (free functions, `module.exports`-style
|
||||
//! handlers) is identical at the runtime level after type erasure. This
|
||||
//! module exists so the [`crate::dynamic::lang::LangEmitter`] dispatch table
|
||||
//! has a discoverable per-language handle and so callers can call
|
||||
//! `entry_kinds_supported(Lang::TypeScript)` symmetrically with the other
|
||||
//! languages — the actual `emit` body delegates to
|
||||
//! [`crate::dynamic::lang::javascript::emit`].
|
||||
//!
|
||||
//! Phase 13 (Track B JS + TS vertical) introduces TS-specific shapes
|
||||
//! (Next.js route handlers, `tsx` browser modules under jsdom). When those
|
||||
//! land, the supported list / hint shift here without affecting the JS
|
||||
//! emitter.
|
||||
|
||||
use crate::dynamic::lang::{javascript, HarnessSource, LangEmitter};
|
||||
use crate::dynamic::spec::{EntryKind, HarnessSpec};
|
||||
use crate::evidence::UnsupportedReason;
|
||||
|
||||
/// Zero-sized [`LangEmitter`] handle for TypeScript.
|
||||
pub struct TypeScriptEmitter;
|
||||
|
||||
/// Entry kinds the TypeScript emitter currently understands. Same as JS until
|
||||
/// Phase 13 introduces TS-specific shapes (Next.js route handlers, `tsx`
|
||||
/// browser modules).
|
||||
const SUPPORTED: &[EntryKind] = &[EntryKind::Function];
|
||||
|
||||
impl LangEmitter for TypeScriptEmitter {
|
||||
fn emit(&self, spec: &HarnessSpec) -> Result<HarnessSource, UnsupportedReason> {
|
||||
javascript::emit(spec)
|
||||
}
|
||||
|
||||
fn entry_kinds_supported(&self) -> &'static [EntryKind] {
|
||||
SUPPORTED
|
||||
}
|
||||
|
||||
fn entry_kind_hint(&self, attempted: EntryKind) -> String {
|
||||
format!(
|
||||
"typescript emitter supports {SUPPORTED:?} (delegates to the JavaScript emitter); this finding's enclosing context is `EntryKind::{attempted}` — Track B will add Next.js / jsdom shapes in phase 13"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn entry_kinds_supported_is_non_empty() {
|
||||
assert!(!TypeScriptEmitter.entry_kinds_supported().is_empty());
|
||||
assert!(TypeScriptEmitter
|
||||
.entry_kinds_supported()
|
||||
.contains(&EntryKind::Function));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn entry_kind_hint_names_attempted_and_phase() {
|
||||
let hint = TypeScriptEmitter.entry_kind_hint(EntryKind::HttpRoute);
|
||||
assert!(hint.contains("HttpRoute"));
|
||||
assert!(hint.contains("phase 13"));
|
||||
}
|
||||
}
|
||||
|
|
@ -47,18 +47,12 @@ pub struct EntryRef {
|
|||
pub function: String,
|
||||
}
|
||||
|
||||
/// What kind of entry point the harness should call.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum EntryKind {
|
||||
/// Free function. Build a `main` that calls it directly.
|
||||
Function,
|
||||
/// HTTP route. Stand up the framework, send a request.
|
||||
HttpRoute,
|
||||
/// CLI subcommand. Spawn the binary with crafted argv.
|
||||
CliSubcommand,
|
||||
/// Library API surface. Build an in-process consumer.
|
||||
LibraryApi,
|
||||
}
|
||||
/// Re-export of [`crate::evidence::EntryKind`].
|
||||
///
|
||||
/// The canonical definition lives in `evidence.rs` so that
|
||||
/// [`crate::evidence::InconclusiveReason::EntryKindUnsupported`] can name the
|
||||
/// attempted / supported variants without depending on the `dynamic` feature.
|
||||
pub use crate::evidence::EntryKind;
|
||||
|
||||
/// Where the payload goes when the harness fires.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
|
@ -210,6 +204,23 @@ impl HarnessSpec {
|
|||
Err(UnsupportedReason::SpecDerivationFailed)
|
||||
}
|
||||
|
||||
/// True when [`HarnessSpec::entry_kind`] is in
|
||||
/// [`crate::dynamic::lang::entry_kinds_supported`] for [`HarnessSpec::lang`].
|
||||
///
|
||||
/// Strategies 1–4 may stamp non-`Function` entry kinds (route handlers,
|
||||
/// CLI subcommands) onto the spec when the rule namespace or the
|
||||
/// resolved [`crate::summary::FuncSummary`] indicates the enclosing
|
||||
/// function is externally driven; not every lang emitter understands
|
||||
/// those shapes yet (Tracks B.12–B.16 add them per language). The
|
||||
/// verifier consults this gate so unsupported shapes route to
|
||||
/// [`crate::evidence::InconclusiveReason::EntryKindUnsupported`] with a
|
||||
/// concrete supported list and hint, rather than degrading silently to
|
||||
/// `Unsupported`.
|
||||
pub fn entry_kind_is_supported(&self) -> bool {
|
||||
let supported = crate::dynamic::lang::entry_kinds_supported(self.lang);
|
||||
supported.contains(&self.entry_kind)
|
||||
}
|
||||
|
||||
/// Returns the ordered list of derivation strategies that
|
||||
/// [`HarnessSpec::from_finding_opts`] attempts. Used by the verifier when
|
||||
/// it needs to report which candidates were tried before declaring an
|
||||
|
|
|
|||
|
|
@ -122,6 +122,41 @@ impl TelemetryEvent {
|
|||
path: Some(diag.path.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Telemetry event for a verdict reached without a [`Diag`] handle.
|
||||
///
|
||||
/// Used by `verify_finding` when emitting an
|
||||
/// `Inconclusive(EntryKindUnsupported)` from inside `build_verdict` —
|
||||
/// the diag is not threaded that far, but the spec's `entry_file` and
|
||||
/// the inconclusive reason carry enough signal to populate the event.
|
||||
/// `cap` and `finding_id` default to empty / `0`; downstream consumers
|
||||
/// already handle that path for `no_spec` events.
|
||||
pub fn no_spec_for_path(
|
||||
path: &str,
|
||||
status: VerifyStatus,
|
||||
inconclusive_reason: Option<InconclusiveReason>,
|
||||
) -> Self {
|
||||
let lang = Path::new(path)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.and_then(crate::symbol::Lang::from_extension)
|
||||
.map(|l| l.as_str().to_owned())
|
||||
.unwrap_or_else(|| "unknown".to_owned());
|
||||
Self {
|
||||
ts: chrono::Utc::now().to_rfc3339(),
|
||||
finding_id: String::new(),
|
||||
spec_hash: String::new(),
|
||||
lang,
|
||||
cap: "0".to_owned(),
|
||||
status: format!("{status:?}"),
|
||||
toolchain_id: String::new(),
|
||||
toolchain_match: String::new(),
|
||||
duration_ms: 0,
|
||||
build_attempts: 0,
|
||||
inconclusive_reason: inconclusive_reason.map(|r| format!("{r:?}")),
|
||||
path: Some(path.to_owned()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write a telemetry event to the events log.
|
||||
|
|
|
|||
|
|
@ -167,6 +167,60 @@ fn insert_verdict_cache(
|
|||
);
|
||||
}
|
||||
|
||||
/// Build an `Inconclusive(EntryKindUnsupported)` verdict for a finding whose
|
||||
/// derived spec named an entry kind the lang emitter does not yet handle.
|
||||
///
|
||||
/// `attempted` is the spec's entry kind; `lang` is the spec's language; the
|
||||
/// supported list and human-readable hint come from the lang emitter via
|
||||
/// [`crate::dynamic::lang::entry_kinds_supported`] /
|
||||
/// [`crate::dynamic::lang::entry_kind_hint`], so adding new shapes in later
|
||||
/// Track B phases automatically narrows what gets routed here without
|
||||
/// touching this function.
|
||||
///
|
||||
/// The caller passes the originating [`Diag`] when one is in scope (for the
|
||||
/// pre-flight gate) or `None` otherwise (for the residual harness-emit path,
|
||||
/// where only the spec is available); telemetry derives `lang`/`path` from
|
||||
/// the diag when present and falls back to the spec otherwise.
|
||||
fn entry_kind_unsupported_verdict(
|
||||
finding_id: String,
|
||||
diag: Option<&Diag>,
|
||||
spec_entry_path: &str,
|
||||
lang: crate::symbol::Lang,
|
||||
attempted: crate::dynamic::spec::EntryKind,
|
||||
) -> VerifyResult {
|
||||
let supported = crate::dynamic::lang::entry_kinds_supported(lang).to_vec();
|
||||
let hint = crate::dynamic::lang::entry_kind_hint(lang, attempted);
|
||||
let inconclusive_reason = InconclusiveReason::EntryKindUnsupported {
|
||||
lang,
|
||||
attempted,
|
||||
supported,
|
||||
hint,
|
||||
};
|
||||
let event = match diag {
|
||||
Some(d) => TelemetryEvent::no_spec(
|
||||
d,
|
||||
VerifyStatus::Inconclusive,
|
||||
Some(inconclusive_reason.clone()),
|
||||
),
|
||||
None => TelemetryEvent::no_spec_for_path(
|
||||
spec_entry_path,
|
||||
VerifyStatus::Inconclusive,
|
||||
Some(inconclusive_reason.clone()),
|
||||
),
|
||||
};
|
||||
telemetry::emit(&event);
|
||||
VerifyResult {
|
||||
finding_id,
|
||||
status: VerifyStatus::Inconclusive,
|
||||
triggered_payload: None,
|
||||
reason: None,
|
||||
inconclusive_reason: Some(inconclusive_reason),
|
||||
detail: None,
|
||||
attempts: vec![],
|
||||
toolchain_match: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Decide whether a [`HarnessSpec::from_finding_opts`] failure should surface
|
||||
/// as `Unsupported` (the finding is genuinely unmodellable) or
|
||||
/// `Inconclusive(SpecDerivationFailed)` (the rule namespace or sink evidence
|
||||
|
|
@ -279,6 +333,21 @@ pub fn verify_finding(diag: &Diag, opts: &VerifyOptions) -> VerifyResult {
|
|||
}
|
||||
};
|
||||
|
||||
// Pre-flight gate: surface a structured `Inconclusive(EntryKindUnsupported)`
|
||||
// up-front when the spec's [`EntryKind`] is not in the lang emitter's
|
||||
// supported list. Without this, the same condition would degrade silently
|
||||
// through `lang::emit -> HarnessError::Unsupported` and lose the
|
||||
// supported-list / hint context the operator needs to triage.
|
||||
if !spec.entry_kind_is_supported() {
|
||||
return entry_kind_unsupported_verdict(
|
||||
finding_id,
|
||||
Some(diag),
|
||||
&spec.entry_file,
|
||||
spec.lang,
|
||||
spec.entry_kind,
|
||||
);
|
||||
}
|
||||
|
||||
// Scan the entry file's directory for sensitive files (§17.3 mount filter).
|
||||
// If the entry file itself matches a sensitive pattern, refuse to run it:
|
||||
// the harness would copy it into the workdir and expose secrets.
|
||||
|
|
@ -498,6 +567,25 @@ fn build_verdict(
|
|||
toolchain_match: None,
|
||||
},
|
||||
Err(RunError::Harness(e)) => {
|
||||
// EntryKindUnsupported coming back from the lang emitter is
|
||||
// promoted to a structured `Inconclusive(EntryKindUnsupported)`
|
||||
// verdict so the operator sees the supported list + hint, not a
|
||||
// bare `Unsupported`. The pre-flight gate in `verify_finding`
|
||||
// catches the common case (entry_kind decided by spec
|
||||
// derivation); this arm covers the residual where an emitter
|
||||
// rejects a payload-slot / shape combination internally.
|
||||
if let crate::dynamic::harness::HarnessError::Unsupported(
|
||||
UnsupportedReason::EntryKindUnsupported,
|
||||
) = &e
|
||||
{
|
||||
return entry_kind_unsupported_verdict(
|
||||
finding_id.to_owned(),
|
||||
None,
|
||||
&spec.entry_file,
|
||||
spec.lang,
|
||||
spec.entry_kind,
|
||||
);
|
||||
}
|
||||
// Typed `Unsupported(reason)` carries its semantics in `reason`; the
|
||||
// free-form `detail` is reserved for `Inconclusive`/unexpected paths
|
||||
// (cf. §10 decision 14 and the verify_result_json_shape contract).
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
use crate::commands::scan::Diag;
|
||||
use crate::patterns::Severity;
|
||||
use crate::symbol::Lang;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt;
|
||||
use std::str::FromStr;
|
||||
|
|
@ -188,6 +189,36 @@ pub enum UnsupportedReason {
|
|||
LangUnsupported,
|
||||
}
|
||||
|
||||
/// What kind of entry point a harness should call.
|
||||
///
|
||||
/// Lives in `evidence.rs` (not `dynamic::spec`) so that
|
||||
/// [`InconclusiveReason::EntryKindUnsupported`] can name the attempted /
|
||||
/// supported variants without depending on the `dynamic` feature. The
|
||||
/// canonical accessor is `crate::dynamic::spec::EntryKind` (re-export).
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub enum EntryKind {
|
||||
/// Free function. Build a `main` that calls it directly.
|
||||
Function,
|
||||
/// HTTP route. Stand up the framework, send a request.
|
||||
HttpRoute,
|
||||
/// CLI subcommand. Spawn the binary with crafted argv.
|
||||
CliSubcommand,
|
||||
/// Library API surface. Build an in-process consumer.
|
||||
LibraryApi,
|
||||
}
|
||||
|
||||
impl fmt::Display for EntryKind {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let s = match self {
|
||||
Self::Function => "Function",
|
||||
Self::HttpRoute => "HttpRoute",
|
||||
Self::CliSubcommand => "CliSubcommand",
|
||||
Self::LibraryApi => "LibraryApi",
|
||||
};
|
||||
f.write_str(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// Spec-derivation strategy attempted by [`crate::dynamic::spec::HarnessSpec::from_finding_opts`].
|
||||
///
|
||||
/// Lives in `evidence.rs` (not `dynamic::spec`) so that
|
||||
|
|
@ -252,6 +283,16 @@ pub enum InconclusiveReason {
|
|||
tried: Vec<SpecDerivationStrategy>,
|
||||
hint: String,
|
||||
},
|
||||
/// The lang-specific harness emitter does not yet support the spec's
|
||||
/// [`EntryKind`]. Carries the language, the attempted entry kind, the
|
||||
/// list of entry kinds the emitter currently understands, and a
|
||||
/// human-actionable hint pointing at the phase that will add support.
|
||||
EntryKindUnsupported {
|
||||
lang: Lang,
|
||||
attempted: EntryKind,
|
||||
supported: Vec<EntryKind>,
|
||||
hint: String,
|
||||
},
|
||||
}
|
||||
|
||||
/// High-level outcome of a dynamic verification attempt.
|
||||
|
|
|
|||
10
src/fmt.rs
10
src/fmt.rs
|
|
@ -527,6 +527,16 @@ fn format_inconclusive_reason(r: &crate::evidence::InconclusiveReason) -> String
|
|||
format!("spec derivation failed ({hint})")
|
||||
}
|
||||
}
|
||||
InconclusiveReason::EntryKindUnsupported {
|
||||
lang,
|
||||
attempted,
|
||||
supported,
|
||||
..
|
||||
} => {
|
||||
format!(
|
||||
"entry kind {attempted} unsupported for {lang:?} (supported: {supported:?})"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue