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");
+    }
+}

Reply via email to