This is an automated email from the ASF dual-hosted git repository. luigidemasi pushed a commit to branch fix/security-model-verification-153-158 in repository https://gitbox.apache.org/repos/asf/camel-kit.git
commit a7ab34039701775c3f2efc0437347f68afb92548 Author: Luigi De Masi <[email protected]> AuthorDate: Mon Nov 9 19:15:20 2026 +0100 Fix #153: verify all graph parsers read sources without execution Fixes #153 Co-authored-by: OpenAI Codex <[email protected]> --- CHANGELOG.md | 2 + docs/security-model.md | 16 +- docs/security-model.yaml | 7 +- docs/security-verification.md | 21 ++ kit/camel-kit-graph/pom.xml | 6 + .../parser/ParserSourceReadingContractTest.java | 324 +++++++++++++++++++++ .../test/java/source/contract/ProjectAction.java | 12 + 7 files changed, 376 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12e39c0d..d84d6f31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- **Parser source-reading regression coverage (#153)** — exercise all nine graph parsers with execution, class-loading, placeholder and external-entity canaries. + - **Google Antigravity native Ship (#95)** — eligible sessions relay controller-issued stages through a dedicated `camel-ship-worker` agent with `commandExecutionPolicy: "off"` and read-only tools. `--backend antigravity-native` reuses the native task/result, recovery, oversight and deterministic publication diff --git a/docs/security-model.md b/docs/security-model.md index d38fe1ca..8427e7e5 100644 --- a/docs/security-model.md +++ b/docs/security-model.md @@ -99,7 +99,7 @@ metadata; Kit configures it for each harness, and it also serves MCP clients tha | Family | Representative entry points | Touches outside the process? | In this model? | | --- | --- | --- | --- | | **A. Workspace CLI** — `init`, `doctor`, `doc`, `nextId`, `plan analyze`, `graph *` (`camel-kit-core` commands, services, generators, catalog downloaders; `camel-kit-main` JBang launcher; `camel-jbang-plugin-kit`) | picocli commands | Writes the target workspace; reads `~/.camel-kit/config.properties`; `init` downloads catalogs over HTTPS unless `--no-fetch`; version probes spawn `java`, `jbang`, `camel`, `mvn`/`mvnw`, `pi` | **In** | - | **B. Graph analysis** (`camel-kit-graph`: Pom, Java, Groovy, XML route, Mule XML, DataWeave, BizTalk, YAML route, Config parsers plus the cross-linker and property-binding post-processors) | `camel-kit graph …`; `init --here` auto-analysis | Reads the analysed tree; writes `.camel-kit/project-graph.json`; no network, no child process *(inferred — V1)* | **In** — the analysed tree is untrusted input | + | **B. Graph analysis** (`camel-kit-graph`: Pom, Java, Groovy, XML route, Mule XML, DataWeave, BizTalk, YAML route, Config parsers plus the cross-linker and property-binding post-processors) | `camel-kit graph …`; `init --here` auto-analysis | Reads the analysed tree; writes `.camel-kit/project-graph.json`; no network, no child process *(documented — `ParserSourceReadingContractTest`; [V1 evidence](security-verification.md#v1-source-reading))* | **In** — the analysed tree is untrusted input | | **C. Skills, templates, traits, agent registry, workflow manifest** (`kit/camel-kit-core/src/main/resources/{skills,templates,agents,workflow}`) | installed by `init` into the workspace | Nothing by itself; the harness executes the instructions with its own permissions; generated MCP configs make the harness launch MCP servers through `jbang` at pinned coordinates | **In** as shipped instructions and generated configuration; the harness and the model are out (§4.3) | | **D. Ship controller** (Technology Preview; `camel-kit-core` `ship/**`, `command/ship`) | `camel-kit ship`, `/camel-ship` delegates | Spawns `pi`/`node` (Pi backend), `/usr/bin/setsid`, `/bin/kill`, its own `java`; writes `.camel-kit/ship/state` or `$CAMEL_KIT_SHIP_STATE_HOME`; publishes into the live project; HTTPS to Maven Central through family E | **In** | | **E. Ship resolver and evidence payloads** (`camel-kit-ship-resolver`; `ship/evidence/**` launchers) | controller-only | HTTPS to Maven Central; private local Maven repository; private tmp/home | **In** | @@ -349,7 +349,7 @@ a report against `0.0.2` is triaged on what that release documents. | Process (configuration) | Listening sockets | Child processes | Network | Filesystem writes | Environment read | Telemetry | Limits | | --- | --- | --- | --- | --- | --- | --- | --- | | **Kit CLI** `camel-kit` / `camel kit`: `init`, `doctor`, `doc`, `nextId`, `plan analyze`, `graph` | none (inspected) | version probes only: `java -version`, `jbang version`, `camel --version`, `mvn`/`mvnw`, `pi -a` *(documented — `PrerequisiteChecker`, `DoctorService`, `docs/agent-architectures.md`)* | `init` without `--no-fetch`: HTTPS to `repo1.maven.org`, `raw.githubusercontent.com/apache/camel`, `api.github.com` (Kamelet listing) and `search.maven.org`; redirects followed except HT [...] -| **Graph analysis** (inside the CLI process) | none (inspected) | none *(inferred — V1)* | none *(inferred — V1)* | `.camel-kit/project-graph.json` or the `--output` path *(documented — `GraphGenerateCommand`)* | none (inspected) | none (inspected) | 60 s per parser except the POM parser; no file-size or memory bound; file symbolic links not yet checked *(documented — `GraphBuilder`; gaps V2)* | +| **Graph analysis** (inside the CLI process) | none (inspected) | none *(documented — `ParserSourceReadingContractTest`; [V1 evidence](security-verification.md#v1-source-reading))* | none *(documented — `ParserSourceReadingContractTest`; [V1 evidence](security-verification.md#v1-source-reading))* | `.camel-kit/project-graph.json` or the `--output` path *(documented — `GraphGenerateCommand`)* | none (inspected) | none (inspected) | 60 s per parser except the POM parser; no file-size or m [...] | **Ship controller** `camel-kit ship` | none (inspected) | `pi` and `node` (Pi backend), `/usr/bin/setsid`, `/bin/kill`, the controller's own `java` for evidence *(documented — `PiWorker`, `LocalCommandRunner`, `EvidenceRunner`)* | HTTPS to Maven Central through the resolver; no redirects; checksum `fail` *(documented — `ShipMavenResolver`)* | `.camel-kit/ship/state/` or `$CAMEL_KIT_SHIP_STATE_HOME`; the live project only through journaled publication *(documented — `docs/commands.md`, [...] | **Ship Pi worker** (`--backend pi`) | Pi's own process | Pi's tools — `bash`, `edit`, `write` in EXECUTE — started in the staging cwd, which is a working directory, not a confinement *(documented — `PiWorker` Javadoc)* | whatever Pi and its provider need — external behaviour, outside this model (§4.3); launched with `--offline`, `--no-extensions`, `--no-skills`, `--no-context-files` *(documented — `PiWorker` arguments)* | *Intended:* the staging workspace and its session directory unde [...] | **Ship evidence JVMs** | none (inspected) | none (inspected) | none by design: every non-direct endpoint stubbed *(documented — `docs/commands.md`)* | private `HOME` and `TMPDIR` under the run; frozen read-only copy *(documented — `EvidenceRunner`)* | scrubbed to `LANG`, `LC_ALL`, `HOME`, `TMPDIR`, `GIT_CEILING_DIRECTORIES` and controller-provided variables *(documented — `EvidenceRunner`)* | none (inspected) | 64 MiB log cap; stage timeout *(documented — `EvidenceRunner`)* | @@ -494,9 +494,9 @@ violation warrants coordinated disclosure and a CVE; **hardening** means it is f use a `DocumentBuilderFactory` with DTDs disallowed, external entities and external DTD loading off, XInclude off and entity expansion off *(documented — `SecureXml`)*; the four BizTalk StAX parsers set `IS_SUPPORTING_EXTERNAL_ENTITIES=false` and `SUPPORT_DTD=false` *(documented — `BizTalk*Parser`)*; the Java parser - is AST-only (JavaParser, no symbol solving) *(inferred — V1)*; the Groovy parser compiles to the `CONVERSION` - phase only *(inferred — V1)*; YAML routes and configuration use Jackson's tree model, DataWeave and properties are - regular-expression scans *(inferred — V1)*. Bound in place: `GraphBuilder` runs every parser other than the POM + is AST-only (JavaParser, no symbol solving) *(documented — `ParserSourceReadingContractTest`; [V1 evidence](security-verification.md#v1-source-reading))*; the Groovy parser compiles to the `CONVERSION` + phase only *(documented — `ParserSourceReadingContractTest`; [V1 evidence](security-verification.md#v1-source-reading))*; YAML routes and configuration use Jackson's tree model, DataWeave and properties are + regular-expression scans *(documented — `ParserSourceReadingContractTest`; [V1 evidence](security-verification.md#v1-source-reading))*. Bound in place: `GraphBuilder` runs every parser other than the POM parser as a task with a 60-second timeout (`DEFAULT_PARSER_TIMEOUT`); a parser that exceeds it is cancelled and recorded as a timeout diagnostic *(documented — `GraphBuilder`)*. Not yet verified: a bound for the POM parser, a memory or file-size bound, and confinement of file reads to the analysed root when the tree contains symbolic @@ -1038,10 +1038,8 @@ routes to one of these items; the model is accepted when each is verified by a m Until then a report that matches an open item is triaged on the model's claim (§4.13). Each item is tracked in its own issue, linked below. -- **V1 — Per-parser regression coverage (Q1; #153) → §4.2, §4.5, §4.8 P1.** Tests pinning, for each of the nine parsers - including the compiler-based Java and Groovy paths, that parsing executes no project code, loads no project class, - resolves no placeholder or secret, opens no connection and spawns no process. *Interim triage:* a demonstrated breach - is `VALID`. +- **V1 — Per-parser regression coverage (Q1; #153): implemented.** `ParserSourceReadingContractTest` exercises all nine parsers, with observable output and hostile source canaries. See [V1 evidence](security-verification.md#v1-source-reading), including mutation controls. A demonstrated breach remains `VALID` under P1. + - **V2 — Graph confinement and resource bounds (Q2; #154) → §4.6, §4.8 P1, P11.** Verify or add: confinement of file reads to the analysed root when the tree contains symbolic links; the POM parser under the parser timeout; a memory or file-size bound. *Interim triage:* a demonstrated outside-root read, non-termination or unbounded memory is `VALID`. diff --git a/docs/security-model.yaml b/docs/security-model.yaml index f92d08a3..4249a50b 100644 --- a/docs/security-model.yaml +++ b/docs/security-model.yaml @@ -281,10 +281,10 @@ configuration_variants: claimed_properties: - id: P1 - property: Source-reading contract for graph analysis, inherited in full from the umbrella model - no code execution, class loading, placeholder/secret resolution, network or child processes; always finishes in bounded time and memory; output is data. Bound in place - GraphBuilder 60-second timeout per parser other than the POM parser. Not yet verified - per-parser regression coverage (V1); POM parser bound, memory or file-size bound, confinement of reads to the analysed root when the [...] + property: Source-reading contract for graph analysis, inherited in full from the umbrella model - no code execution, class loading, placeholder/secret resolution, network or child processes; always finishes in bounded time and memory; output is data. Bound in place - GraphBuilder 60-second timeout per parser other than the POM parser. Not yet verified - POM parser bound, memory or file-size bound, confinement of reads to the analysed root when the tree contains symbolic links (V2). violation_symptom: Child process, class loaded or defined from the source, outbound connection, read of a file outside the analysed root (including through a symbolic link), placeholder resolved, non-termination, or unbounded memory during graph analysis. severity: security-critical (VALID regardless of the verification status in V1/V2) - provenance: contract - documented (umbrella) and maintainer (2026-10-27, Q1 and Q2); XML/StAX hardening documented (SecureXml, BizTalk parsers); GraphBuilder timeout documented; Java, Groovy, YAML, DataWeave, properties enforcement inferred (V1); confinement and resource enforcement inferred (V2) + provenance: contract - documented (umbrella) and maintainer (2026-10-27, Q1 and Q2); XML/StAX hardening documented (SecureXml, BizTalk parsers); GraphBuilder timeout documented; all nine parsers covered by ParserSourceReadingContractTest (security-verification.md V1); confinement and resource enforcement inferred (V2) - id: P2 property: Hardened XML parsing at every XML entry point, including Ship pom.xml validation. violation_symptom: XXE, SSRF or local file read through XML. @@ -580,7 +580,8 @@ verification_items: sections: ["4.2", "4.8 P1"] work: Tests pinning, for each of the nine parsers including the compiler-based Java and Groovy paths, that parsing executes no project code, loads no project class, resolves no placeholder or secret, opens no connection and spawns no process. interim_triage: A demonstrated breach is VALID under P1. - status: open + status: "implemented" + evidence: "ParserSourceReadingContractTest: nine parser fixtures, four XML external-entity cases, calibrated JFR/process/class observers and loopback network canary; security-verification.md V1" - id: V2 tracking: apache/camel-kit#154 title: Graph confinement and resource bounds diff --git a/docs/security-verification.md b/docs/security-verification.md new file mode 100644 index 00000000..8af1e3fd --- /dev/null +++ b/docs/security-verification.md @@ -0,0 +1,21 @@ +# Security model verification + +Evidence for issues #153–#158. These are automated regression results and source references, +not a claim of human maintainer approval. The security model remains the policy authority. +Tests characterize the stated configuration; neither source inspection nor a passing probe +establishes an OS sandbox. Re-run the named tests when their implementation or dependencies change. + +## V1: source reading + +`kit/camel-kit-graph/.../ParserSourceReadingContractTest` invokes all nine parsers with +nonempty graph assertions. Fixtures include Java/Groovy code and AST transform canaries, +YAML object tags, external XML entities, runtime-looking placeholders and a secret sentinel. +JFR observes process starts and project-class loads; a loopback HTTP server observes external +entity attempts. The positive control deliberately performs each observed side effect. + +The 14 cases passed directly on Java 17, 21 and 25. Isolated mutations moving Groovy compilation +to semantic analysis, substituting a runtime property, loading a project class, starting a +process and enabling XML entity expansion all caused failures. The XML mutation uses element +text: an external entity in an attribute would be rejected even by an otherwise unsafe parser. +These probes protect the covered execution paths; they do not prove safety of arbitrary future +parser/dependency changes. Website impact: not required; this pins existing behavior. diff --git a/kit/camel-kit-graph/pom.xml b/kit/camel-kit-graph/pom.xml index a63f9aca..fa8cd414 100644 --- a/kit/camel-kit-graph/pom.xml +++ b/kit/camel-kit-graph/pom.xml @@ -54,6 +54,12 @@ </dependency> <!-- Test --> + <dependency> + <groupId>de.thetaphi</groupId> + <artifactId>forbiddenapis</artifactId> + <version>${forbiddenapis.version}</version> + <scope>test</scope> + </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> diff --git a/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/ParserSourceReadingContractTest.java b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/ParserSourceReadingContractTest.java new file mode 100644 index 00000000..0d8f38f8 --- /dev/null +++ b/kit/camel-kit-graph/src/test/java/org/apache/camel/kit/graph/parser/ParserSourceReadingContractTest.java @@ -0,0 +1,324 @@ +package org.apache.camel.kit.graph.parser; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicInteger; + +import org.apache.camel.kit.graph.ProjectGraph; + +import com.sun.net.httpserver.HttpServer; +import de.thetaphi.forbiddenapis.SuppressForbidden; +import jdk.jfr.Recording; +import jdk.jfr.consumer.RecordedEvent; +import jdk.jfr.consumer.RecordingFile; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import static org.junit.jupiter.api.Assertions.*; + +class ParserSourceReadingContractTest { + + private static final String SECRET_PROPERTY = "camel.kit.source.contract.secret"; + private static final String SECRET_VALUE = "source-contract-runtime-secret"; + private static final String PLACEHOLDER = "${" + SECRET_PROPERTY + "}"; + private static final String PROJECT_CLASS = "source.contract.ProjectAction"; + + @TempDir + Path tempDir; + + @ParameterizedTest + @EnumSource(ParserCase.class) + // Public JFR APIs observe process and class loading on the supported JDKs (17, 21, and 25). + @SuppressForbidden + void parsingDoesNotRunProjectActionsOrResolveRuntimeValues(ParserCase parserCase) throws Exception { + Path project = Files.createDirectory(tempDir.resolve("project")); + Path marker = tempDir.resolve("executed"); + String previous = System.setProperty(SECRET_PROPERTY, SECRET_VALUE); + try (NetworkCanary network = new NetworkCanary()) { + String actions = actions(marker, network.uri()); + String source = parserCase.source(actions, network.uri()); + Files.writeString(project.resolve(parserCase.fileName), source); + ProjectGraph graph = new ProjectGraph(); + GraphParser parser = parserCase.parser(); + + List<RecordedEvent> events = record(() -> parser.parse(project, graph)); + + assertTrue(graph.hasNode(parserCase.expectedNode), "The fixture must actually be parsed"); + assertTrue(parser.warnings().isEmpty(), () -> parser.warnings().toString()); + assertFalse(graph.getNodes().toString().contains(SECRET_VALUE), + "Runtime system properties must not replace source placeholders"); + if (parserCase != ParserCase.DATAWEAVE) { + assertTrue(graph.getNodes().toString().contains(PLACEHOLDER), + "The placeholder must remain literal in the graph"); + } + assertFalse(Files.exists(marker), "Source code and compile-time annotations must not execute"); + assertEquals(0, network.requests.get(), "Source URLs must never be fetched"); + assertFalse(events.stream().anyMatch(event -> event.getEventType().getName().equals("jdk.ProcessStart")), + "Parsing must not start a child process"); + assertFalse(events.stream().anyMatch(event -> event.getEventType().getName().equals("jdk.ClassLoad") + && event.getClass("loadedClass").getName().equals(PROJECT_CLASS)), + "Parsing must not load a project class"); + assertEquals(source, Files.readString(project.resolve(parserCase.fileName))); + } finally { + if (previous == null) { + System.clearProperty(SECRET_PROPERTY); + } else { + System.setProperty(SECRET_PROPERTY, previous); + } + } + } + + @ParameterizedTest + @EnumSource(value = ParserCase.class, names = {"POM", "XML", "MULE", "BIZTALK"}) + void xmlParsersDoNotFetchExternalEntities(ParserCase parserCase) throws Exception { + Path project = Files.createDirectory(tempDir.resolve("project")); + try (NetworkCanary network = new NetworkCanary()) { + String source = parserCase.source("", network.uri()); + String root = switch (parserCase) { + case POM -> "project"; + case XML -> "routes"; + case MULE -> "mule"; + case BIZTALK -> "Document"; + default -> throw new IllegalArgumentException(); + }; + String entitySource = parserCase == ParserCase.XML || parserCase == ParserCase.MULE + ? source.replace("<![CDATA[]]>", "&external;") + : source.replace(PLACEHOLDER, "&external;"); + Files.writeString(project.resolve(parserCase.fileName), + "<!DOCTYPE " + root + " [<!ENTITY external SYSTEM \"" + network.uri() + "\">]>\n" + + entitySource); + ProjectGraph graph = new ProjectGraph(); + try { + parserCase.parser().parse(project, graph); + } catch (RuntimeException expectedRejection) { + // POM rejects an unresolved entity; the XML families report parser warnings instead. + } + assertEquals(0, network.requests.get()); + assertFalse(graph.getNodes().values().stream() + .flatMap(node -> node.properties().values().stream()) + .anyMatch(value -> value.contains("entity-body"))); + } + } + + @Test + // The positive control reads the same public JFR events as the parser checks. + @SuppressForbidden + void sideEffectObserversDetectRealNetworkProcessAndClassLoading() throws Exception { + // Test discovery may preload nested classes; a fresh loader makes this control independent of discovery order. + try (NetworkCanary network = new NetworkCanary(); + URLClassLoader loader = new URLClassLoader( + new URL[]{getClass().getProtectionDomain().getCodeSource().getLocation()}, + ClassLoader.getPlatformClassLoader())) { + List<RecordedEvent> events = record(() -> { + try (var input = network.uri().toURL().openStream()) { + assertEquals("entity-body", + new String(input.readAllBytes(), java.nio.charset.StandardCharsets.UTF_8)); + } + Process child = new ProcessBuilder(javaExecutable(), "-version") + .redirectOutput(ProcessBuilder.Redirect.DISCARD) + .redirectError(ProcessBuilder.Redirect.DISCARD).start(); + assertTrue(child.waitFor(10, java.util.concurrent.TimeUnit.SECONDS)); + assertEquals(0, child.exitValue()); + Class.forName(getClass().getName() + "$ClassLoadingCanary", true, loader); + }); + assertEquals(1, network.requests.get()); + assertTrue(events.stream().anyMatch(event -> event.getEventType().getName().equals("jdk.ProcessStart"))); + assertTrue(events.stream().anyMatch(event -> event.getEventType().getName().equals("jdk.ClassLoad") + && event.getClass("loadedClass").getName().endsWith("$ClassLoadingCanary"))); + } + } + + // Public JFR recording APIs are available on all supported JDKs. + @SuppressForbidden + private List<RecordedEvent> record(CheckedAction action) throws Exception { + Path recordingPath = tempDir.resolve("source-reading.jfr"); + try (Recording recording = new Recording()) { + recording.enable("jdk.ProcessStart").withThreshold(Duration.ZERO); + recording.enable("jdk.ClassLoad").withThreshold(Duration.ZERO); + recording.start(); + action.run(); + recording.stop(); + recording.dump(recordingPath); + } + return RecordingFile.readAllEvents(recordingPath); + } + + private static String actions(Path marker, URI uri) { + return "java.nio.file.Files.writeString(java.nio.file.Path.of(\"" + quote(marker.toString()) + + "\"), \"executed\"); new java.net.URL(\"" + uri + + "\").openStream().close(); new ProcessBuilder(\"" + quote(javaExecutable()) + + "\", \"-version\").start().waitFor();"; + } + + private static String quote(String value) { + return value.replace("\\", "\\\\").replace("\"", "\\\""); + } + + private static String javaExecutable() { + return Path.of(System.getProperty("java.home"), "bin", + System.getProperty("os.name").startsWith("Windows") ? "java.exe" : "java").toString(); + } + + private enum ParserCase { + POM("pom.xml", "maven:contract:source"), + JAVA("ProjectAction.java", "class:" + PROJECT_CLASS), + GROOVY("ProjectAction.groovy", "class:" + PROJECT_CLASS), + XML("routes.xml", "route:contract"), + MULE("flow.xml", "mule-flow:contract"), + DATAWEAVE("transform.dwl", "dataweave:transform.dwl"), + BIZTALK("contract.btp", "biztalk-pipeline:" + PLACEHOLDER), + YAML("routes.yaml", "route:contract"), + CONFIG("application.properties", "config:contract.secret"); + + private final String fileName; + private final String expectedNode; + + ParserCase(String fileName, String expectedNode) { + this.fileName = fileName; + this.expectedNode = expectedNode; + } + + GraphParser parser() { + return switch (this) { + case POM -> new PomParser(); + case JAVA -> new JavaGraphParser(); + case GROOVY -> new GroovyGraphParser(); + case XML -> new XmlRouteParser(); + case MULE -> new MuleXmlFlowParser(); + case DATAWEAVE -> new DataWeaveParser(); + case BIZTALK -> new BizTalkParser(); + case YAML -> new YamlRouteParser(); + case CONFIG -> new ConfigParser(); + }; + } + + String source(String actions, URI uri) { + return switch (this) { + case POM -> + String.format(Locale.ROOT, + """ + <project><modelVersion>4.0.0</modelVersion><groupId>contract</groupId> + <artifactId>source</artifactId><version>%s</version> + <repositories><repository><id>canary</id><url>%s</url></repository></repositories> + <build><plugins><plugin><groupId>org.codehaus.mojo</groupId><artifactId>exec-maven-plugin</artifactId> + <configuration><executable>java</executable><arguments><argument>-version</argument></arguments></configuration> + <executions><execution><phase>validate</phase><goals><goal>exec</goal></goals></execution></executions> + </plugin></plugins></build></project> + """, + PLACEHOLDER, uri); + case JAVA -> String.format(Locale.ROOT, """ + package source.contract; + public class ProjectAction { + static { try { %s } catch (Exception e) { throw new RuntimeException(e); } } + public void configure() { from("direct:%s").to("%s"); } + } + """, actions, PLACEHOLDER, uri); + case GROOVY -> String.format(Locale.ROOT, """ + package source.contract + @groovy.transform.ASTTest(phase=org.codehaus.groovy.control.CompilePhase.SEMANTIC_ANALYSIS, + value={ %s }) + class ProjectAction { + static { %s } + void configure() { from('direct:%s').to('%s') } + } + """, actions, actions, PLACEHOLDER, uri); + case XML -> String.format(Locale.ROOT, """ + <routes xmlns="http://camel.apache.org/schema/spring"> + <route id="contract"><from uri="direct:%s"/><to uri="%s"/> + <setBody><groovy><![CDATA[%s]]></groovy></setBody><bean ref="%s"/> + </route> + </routes> + """, PLACEHOLDER, uri, actions, PROJECT_CLASS); + case MULE -> + String.format(Locale.ROOT, + """ + <mule xmlns="http://www.mulesoft.org/schema/mule/core" + xmlns:http="http://www.mulesoft.org/schema/mule/http" + xmlns:scripting="http://www.mulesoft.org/schema/mule/scripting"> + <http:request-config name="%s"/> + <flow name="contract"><http:request config-ref="%s" url="%s"/> + <scripting:execute engine="groovy"><scripting:code><![CDATA[%s]]></scripting:code></scripting:execute> + </flow> + </mule> + """, + PLACEHOLDER, PLACEHOLDER, uri, actions); + case DATAWEAVE -> String.format(Locale.ROOT, """ + %%dw 2.0 + output application/json + var remote = readUrl("%s", "text/plain") + --- + { secret: p("%s"), data: remote, value: payload.order.id } + """, uri, SECRET_PROPERTY); + case BIZTALK -> String.format(Locale.ROOT, """ + <Document PolicyFilePath="%s"><FriendlyName>%s</FriendlyName> + <Stages><Stage><Components><Component Name="%s" ComponentName="canary" Version="1.0" + Description="%s"/></Components></Stage></Stages> + </Document> + """, uri, PLACEHOLDER, PROJECT_CLASS, uri); + case YAML -> String.format(Locale.ROOT, """ + - route: !!source.contract.ProjectAction + id: contract + from: + uri: 'direct:%s' + steps: + - to: '%s' + - groovy: '%s' + - bean: + ref: '%s' + """, PLACEHOLDER, uri, actions, PROJECT_CLASS); + case CONFIG -> String.format(Locale.ROOT, """ + contract.secret=%s + camel.component.http.address=%s + camel.beans.action=#class:%s + contract.action=%s + """, PLACEHOLDER, uri, PROJECT_CLASS, actions); + }; + } + } + + // The public jdk.httpserver module supplies a loopback-only network observer on the supported JDKs. + @SuppressForbidden + private static final class NetworkCanary implements AutoCloseable { + private final AtomicInteger requests = new AtomicInteger(); + private final HttpServer server; + + NetworkCanary() throws IOException { + server = HttpServer.create(new InetSocketAddress("127.0.0.1", 0), 0); + server.createContext("/", exchange -> { + requests.incrementAndGet(); + byte[] body = "entity-body".getBytes(java.nio.charset.StandardCharsets.UTF_8); + exchange.sendResponseHeaders(200, body.length); + try (var output = exchange.getResponseBody()) { + output.write(body); + } + }); + server.start(); + } + + URI uri() { + return URI.create("http://127.0.0.1:" + server.getAddress().getPort() + "/canary"); + } + + @Override + public void close() { + server.stop(0); + } + } + + private interface CheckedAction { + void run() throws Exception; + } + + private static class ClassLoadingCanary { + } +} diff --git a/kit/camel-kit-graph/src/test/java/source/contract/ProjectAction.java b/kit/camel-kit-graph/src/test/java/source/contract/ProjectAction.java new file mode 100644 index 00000000..c0cac42d --- /dev/null +++ b/kit/camel-kit-graph/src/test/java/source/contract/ProjectAction.java @@ -0,0 +1,12 @@ +package source.contract; + +/** A class named by hostile source fixtures, available so accidental class loading is observable. */ +public final class ProjectAction { + static { + throwIfInitialized(); + } + + private static void throwIfInitialized() { + throw new AssertionError("Graph parsing initialized a project class"); + } +}
