tombaldwin/candor-java

GitHub: tombaldwin/candor-java

candor-java 是一个基于 ASM 的 JVM bytecode 静态分析工具,通过传递性地计算每个方法的副作用映射,在 CI 中强制执行可配置的架构边界不变量。

Stars: 0 | Forks: 0

# candor-java

Beaky, the candor canary

**强制守住 AI 生成的 JVM 代码悄悄跨越的架构边界 —— 作为一道你可以信任的 CI 门禁。** candor-java 通过 [ASM](https://asm.ow2.io/) 读取已编译的 bytecode,并知道哪些方法会触及网络、文件系统、数据库、子进程、环境 —— 以*传递*的方式 —— 然后将诸如*"domain 层不进行 I/O"* 或 *"domain 不得依赖 infra"* 之类的不变量转化为一个 `CANDOR_POLICY`,当某次编辑破坏它们时(`deny`/`pure`/`allow`/`forbid`,AS-EFF-006/008/009),该策略会**让构建失败**。 这是一个 [candor-spec](https://github.com/tombaldwin/candor-spec) 的实现;是 Rust 参考实现 [candor](https://github.com/tombaldwin/candor-rust) 的同胞项目 —— 相同的分类器思想,采用了 JVM 的颗粒度(bytecode + Spring)。 **网站:** [candor.poly.io](https://candor.poly.io) — 五分钟内的实测案例: 展示、预注册评估,以及在您自己的代码库上验证它的路径。 **适用于任何 JVM 语言 — Java, Kotlin, Scala, Groovy。** 因为它读取的是 *bytecode*,candor-java 是语言无关的:这四种语言最终都会编译成它所分析的相同的 `.class` 文件,使用分类器所知晓的相同的 JDK I/O API(`java.net`/`java.nio`),相同的 Spring annotation,以及相同的 JVM 级别的 `Runnable`/`main`。Java 是经过最充分实战检验的(一个真正的包含 2,257 个类的 Spring 应用 + 数百个库 jar 包)。**Kotlin, Scala 和 Groovy 已在真实的 bytecode 上完成验证:** Kotlin(okhttp, ktor, kotlinx-coroutines)能检测到网络 I/O 并将基于 `Runnable` 的 dispatcher 标记为 entry point; Scala(scala-library, cats)和 Groovy(groovy runtime, groovy-json)解析时不会崩溃,能将真实的副作用归因于其真正的来源(`scala.sys.process` → Exec,`Source.fromURL` → Net),并将动态作用面(Scala 广泛的 collection dispatch,Groovy 的 MOP/metaclass)真实地标记为 `Unknown`,而不是悄悄放行。*(那次验证过程修复了两个真实的引擎 bug —— `System.getProperty` 曾被误算为 `Env`,以及在深层继承体系上的 CHA 会导致无限制的扇出;现在两者都已修复,对所有 dispatch 应用了有界 CHA `≤12`-或-`Unknown` 的原则。)* 注意事项:一个充满接口的*被孤立分析的库*(stdlib/runtime 本身)可能会通过 class-init 链条过度报告(参见 下文的 **Not yet**)—— 但一个真正的*应用*,其 runtime 作为未分析的依赖,则不会出现这种情况(cats 分析得很干净:全 `Unknown` 的 typeclass dispatch,零捏造的副作用)。 **一道门禁只有在它从不说谎时才值得信任。** candor-java 会将其无法看到的内容 —— reflection、 `native` body、对未知 impl 的 dispatch —— 标记为 `Unknown`,而绝不是悄悄地标记为“pure”。该契约由 CI 中的一个对抗性 [soundness fuzzer](soundness/) 维系,它将一个已知的副作用穿透每一种 JVM 调用形式(direct / lambda / method-ref / constructor / static-init / interface dispatch / anon class),如果有任何可达的方法返回 pure 则判定失败。因此,当 candor-java 认证某一层是干净的时, 您就可以放心采取行动。 **它还能进行映射** —— 提供针对每个方法的副作用审计,以及针对报告的即时 `show`/`where`/`callers`/`map`/`diff`/ `containment`/`reachable`/`path`/`impact` 查询,方便 agent 或人类在陌生的代码中导航。 ## 状态:alpha (v0.4.x) 已在一个真实的 2,257 个类的 Spring 应用以及真实的 Kotlin/Scala/Groovy bytecode 上完成验证;保持了 规范中的跨引擎一致性测试套件(在 CI 中与 Rust 引擎使用相同的 fixture 和预期的副作用集合);由对抗性的 soundness fuzzer 守护。处于 alpha 阶段是因为分类器和 Spring 表面仍在不断扩展 —— 信任契约(§4:绝不悄悄标记为 pure)并不是不成熟的地方。 **有效功能:** 审计模式 —— 解析每一次调用,将其与副作用表进行对比分类(匹配的是 I/O 边界,而非包),并在 call graph 上传递至不动点。输出 **v0.2 自描述报告** —— 一个 `{ candor, functions }` 信封,其头部携带引擎 构建 ID(git 短哈希,在构建时写入 jar 包中,因此它反映的是*实际运行的二进制文件*) 和工具链(candor-spec §2/§2.1)。读取器仍然接受旧版 v0.1 的纯数组(基线 防护器两者都加载)。每个条目还携带与副作用相关的 **`calls`** 图 —— 即其具有副作用的本地被调用者 —— 因此消费者无需重新分析即可直接从报告中回答“谁调用了 X?”。 **感知 Spring。** Spring 将副作用隐藏在框架编织/生成的代码中(`@Transactional` 的事务存在于 runtime 代理中;Spring Data repository 的实现是在 runtime 合成的)并且 切断了 call graph(controller/listener 是通过反射调用的)。单纯的 bytecode 追踪会漏掉 所有这些 —— 因此 candor-java 转而读取 Spring 的**声明**: - `@Transactional`(方法或类)→ `Db` - Spring Data repository(`extends CrudRepository`/`JpaRepository`)→ 对它们的调用为 `Db` - `RestTemplate` / `WebClient` / `@FeignClient` → `Net`;`JdbcTemplate` / `EntityManager` → `Db`; `JmsTemplate` / `KafkaTemplate` → `Net`;`Environment.getProperty` → `Env` - **Entry point**(runtime 调用的根,无项目调用点):`@*Mapping`,`@Scheduled`, `@*Listener`/`@EventListener`;bean 生命周期 `@PostConstruct`/`@PreDestroy` + `InitializingBean`/ `DisposableBean` + `CommandLineRunner`/`ApplicationRunner`;servlet/filter/listener;JPA entity 回调(`@PrePersist`/…);`Runnable`/`Thread`/`Callable` 任务 body;`finalize()`;以及 **Ktor** 路由处理器(一个 Kotlin `SuspendLambda`,其接收者是 `RoutingContext`/`PipelineContext` — 即 Ktor 从其 pipeline 调用的 `get("/x") { … }` body)。`reachable` 查询在这些基础上对副作用进行合并, 以展示应用在 runtime 的实际行为。 在 `spring-sample/` 上,`register()`(一个调用了 Spring Data repo + `RestTemplate` 的 `@Transactional` 方法)正确推断出 `{ Db, Net }`,而 `@GetMapping` controller 继承了 `{ Db*, Net* }` 并被标记为 `[entry]` —— 这些副作用存在于 candor 能够看到的任何方法 body 之外。 **信任契约 (candor-spec §4)。** candor-java 将其无法看到的内容标记为 `Unknown` (`unresolved: true`),绝不悄悄标记为 pure: - **Reflection / 动态调用**(`Method.invoke`,`Constructor.newInstance`, `Class.forName`/`newInstance`,`MethodHandle.invoke`,`Proxy.newProxyInstance`)→ `Unknown` — 而 Groovy 的 metaclass dispatch(`MetaClass`/`GroovyObject.invokeMethod`,`GroovyShell.evaluate`), 这本身就是 reflection,同样 → `Unknown`。 - **`native` 方法** —— 一个 candor 无法看到的 JNI body 可以执行任何副作用,因此它是 `Unknown` (其调用者会继承它),而绝不是一个空的 bytecode body 看起来的那种 no-op。 - **Class Hierarchy Analysis (CHA)** 将项目类型上的 interface/virtual dispatch 解析为具体的实现,因此副作用会*穿透* dispatch 进行传播(对 `Greeter` interface 的调用会继承其所有实现副作用的并集)。对**没有可见 impl 的项目 interface/abstract**(DI 注入的、外部的、strategy 模式的)的 dispatch → `Unknown`。 CHA 是通过 ASM 已经提供给我们的 class hierarchy 完成的 —— 不需要 WALA/SootUp。 **Pure-exempt dispatch 集合(candor-spec §4 要求对此进行记录)。** 有三种 dispatch 形状被特意*不*进行 CHA 扇出,因为在它们之上,jar 包中的每一个(或几乎每一个)类都是候选目标,一个有副作用的 override 会波及整个报告: - `toString()` / `hashCode()` / `equals(Object)` / `compareTo(Object)` —— 传统上 pure 的 `Object` 表面(这与 Rust 引擎对 `dyn Display`/`Error` 格式化的取舍相同)。 *已记录的注意事项:*对这些方法执行了真实 I/O 的重写不会在 dispatch 点进行归因(其自身的方法条目仍然带有副作用)。 - `kotlin.jvm.functions.FunctionN.invoke`,`scala.FunctionN`/`PartialFunction` `apply`,以及 `groovy.lang.Closure.call` —— 每个 Kotlin/Scala/Groovy 的 lambda 或闭包都是实现这些接口的类;其 body 转而在其**创建点**进行归因(构造即精确)。 - 外部接口上的 `java.lang.Runnable.run` / `Callable.call` —— 任务 body 是 **entry point**(runtime 会调用它们),因此它们的副作用永远不会成为孤儿;event loop 的 `task.run()` 不会被强加 jar 包中每个任务的副作用。 所有其他的 dispatch —— 包括对带有项目实现的外部 interface(`java.util.Iterator`)的 dispatch —— 仍然会进行 CHA 解析。 **跨 jar 包(多模块)。** 每个条目都带有一个稳定的、包含描述符的 `hash` (`owner/Class.method(desc)ret` —— 即调用点使用的确切引用),因此依赖模块可以跨 jar 包边界继承依赖项的副作用(candor-spec §2)。将 `CANDOR_DEPS` 指向 依赖项的报告(一个路径列表,或一个包含 `*.json` 的目录);对单独分析的依赖项的调用随后将继承其记录的副作用,而不是被假定为 pure。**版本感知信任** (§2.1):来自由*不同*引擎版本生成的报告的副作用会被降级为 `Unknown`,而不是被默默信任。 ``` CANDOR_DEPS="/path/to/dep-report.json:/path/to/more" \ gradle run --args="/path/to/app-classes --json /tmp/app.json" ``` **单命令依赖扫描 — `./candor --deps `.** 与其手动扫描每个 依赖 jar 包,不如将 `--deps` 指向 classpath(一个以 `:` 分隔的 jar 列表,或一个 jar 目录):wrapper 会将每个 jar 扫描到 `.candor/deps/` 下的同级报告中,然后链式地在此基础上运行应用扫描 —— 这是依赖扫描最终方案在 JVM 端的体现(一个依赖项的副作用来源于*它自身*对 JDK 边界的调用,因此精选的分类器会向仅限内置的方向缩减)。已缓存:未更改的 jar 的报告会被重用。*Shell* 负责展开扫描,因此引擎本身从不生成进程 (它自身的边界仅为 Fs/Env,spec §7.12)。 ``` ./candor --deps libs/ build/classes/java/main --json /tmp/app.json # scan libs/*.jar, then the app ``` *解析深度:* 一个具体类型的跨 jar 调用通过 `hash` 直接解析;一个 **interface** 类型的 调用,如果其实现在依赖项中,则无法仅凭报告进行去虚化(报告不包含 class hierarchy)。为了实现跨边界的完全解析,请将应用**和**其依赖*一起*分析 (作为一个 classpath)—— 这样本地 CHA 就能看透 dispatch。`CANDOR_DEPS` 是当你只拥有依赖项的*报告*而非其 bytecode 时使用的模式。 **暂未实现(如实推迟 — PRINCIPLES #7):** - 对分类器无法识别的**非项目(JDK/库)类型**的 dispatch 被假定为 pure 而不是 `Unknown` —— 否则每个 `list.add()` 都会淹没报告(这是 Rust 实现中学习到的校准方法)。已知具有副作用的库会被分类器捕获;其余部分是 已记录的遗留缺口。 - 跨 jar 包的 **``**(位于*依赖项*中,仅能通过报告触达的 static initializer)未被归因 —— 因为下文提到的本地触发边需要依赖项的 bytecode 位于 classpath 上。 - **对通用 interface 方法的 CHA 会在充满接口的库上引发副作用泛滥。** 对 interface 类型接收者的调用(`Iterator.next`,`Function.apply`)会通过 Class Hierarchy Analysis 解析为分析集中的*每一个*实现 —— 这在理论上是可靠的(任何实现都有可能在那里),但当其中一个实现执行了 I/O 且该方法普遍调用时,副作用就会泛滥。自我分析 `scala-library` 时,会报告约 74% 的 函数具有 `Net`/`Fs` 副作用,因为一个读取 URL 的 `Iterator` 实现可以通过 CHA 从每一个 iterator 遍历中触达。Rust 实现通过将其*去虚化*为具体类型来避免这一点(编译器为其提供了单态化的接收者);bytecode 的 CHA 在没有类型流分析的情况下无法做到同等效果。在 实践中这是有界的:一个真实的 Scala/Kotlin *应用*将 stdlib 作为未分析的依赖项,因此这些 调用是 `Unknown`,而不是泛滥 —— 它只在库的*自我*分析时才会发作。(在这里计数会产生误导,这正是 candor 报告结构而非“分数”的原因 —— 参见 `containment`。) **Constructors、static initializer 以及 lambda/method reference** *已得到处理* —— 具有副作用的部分会 将其传播到使用点,而不是看起来很 pure: - **constructor**(``)body 的副作用会触达每一个 `new X()` 的位置; - **static initializer**(``)body 的副作用会触达每一个触发类加载的位置 —— `new C`,对 C 的 static 方法调用,或对 C 的 static 字段访问(JVM 在首次使用时运行一次 ``;candor 从每个触发器连边到它,进行了可靠的过近似); - **lambda / method reference** 的实现方法(一个项目 `lambda$…` synthetic 或被引用的方法) 会从外围方法连边过来,因此具有副作用的 `() -> …` 或 `Foo::bar` 会传播其副作用。 最深层的 JVM 天花板在于,即使是 declarations + CHA 也无法捕获的内容:**自定义**的 AOP aspect (`@Aspect`/`@Around`)和通过 reflection 装配的 bean(`getBean`)。解决这些问题的重量级路线是 Spring Boot 3 **AOT metadata**(Spring 自身的提前处理会在构建时解析 bean 装配、proxy 和 reflection)。参见 candor-spec CLASSIFIER.md。 ## 构建与运行 **零安装(推荐):** 通过 [jbang](https://www.jbang.dev) 使用发布的 fat-jar —— 无需 clone, 无需 Gradle: ``` jbang candor@tombaldwin/candor-java /path/to/classes-or-jar --json /tmp/report.json ``` **AI agent?** 将其指向 [AGENTS.md](AGENTS.md) —— 自包含的生成/查询/信任 说明。**持怀疑态度?** [PROVE-IT.md](PROVE-IT.md) 是你的 agent 在你自己的代码库上运行的 15 分钟自我实验。 从源码构建:需要 JDK 21 + Gradle。 ``` # 将你想要分析的代码编译到一个包含 .class 文件的目录中(或指向一个 jar),然后: gradle run --args="/path/to/classes --json /tmp/report.json" # …或者使用 ./candor 便捷包装器(构建一次,在源码更改时自愈): ./candor /path/to/classes --json /tmp/report.json # analyze ./candor show /tmp/report.json myMethod # query a report ``` 它会打印每个方法的副作用审计并写入一份 candor JSON 报告。`bash test/smoke.sh` 运行 行为测试套件,而 `bash soundness/run.sh` 运行[对抗性 soundness fuzzer](soundness/) —— 它会将一个已知的副作用穿透每一种 JVM 调用形式(direct / lambda / method-ref / constructor / static-init / interface dispatch / anon class),并断言 candor-java 绝不会将可达方法报告为 pure(只能是 effect-or-`Unknown`)。两者都在 CI 中运行;fuzzer 的有效性已通过牙齿验证(撤销修复会导致其对应的通道失败)。 ## 查询(只读,针对已写入的报告) 一旦你写入了一份报告(`--json report.json`),你就可以**无需重新分析**就能回答关于它的问题 —— 这是 Rust 实现中 `candor-query` 的同胞工具。它们读取报告自身的字段(`inferred`/`direct`,以及 `calls` 效果图),外加完整的 call-graph **sidecar** (`report.callgraph.json`,会自动生成在报告旁边),用于评估编辑前的爆炸半径: ``` gradle run --args="show report.json " # a function's effects (* = own body; Fs(read,write) detail) gradle run --args="where report.json " # who performs an effect (direct vs inherited) gradle run --args="callers report.json " # the blast radius: who TRANSITIVELY calls a fn — # works for ANY fn incl. PURE ones (pre-edit: # "who is affected if I add an effect here?") gradle run --args="map report.json" # class → effects overview, most-effectful first gradle run --args="diff report.json " # per-function effect delta (+gained / -lost) gradle run --args="containment report.json [baseline.json]" # effect-leakage diagnostic + a ratchet gradle run --args="reachable report.json" # what the app DOES at runtime (union over entry points) gradle run --args="path report.json " # the call chain by which a fn comes to perform an effect gradle run --args="impact report.json " # blast radius: transitive callers + downstream entry points gradle run --args="whatif report.json [policy]" # PRE-EDIT verdict: if I add here, what # propagates AND does it break the deny/pure gate? (exit 1 if so) gradle run --args="rewire report.json " # DE-WIRING: which methods dropped a call vs the # baseline — catch a 'fix' that games the gate by disconnecting ``` 为任何查询添加 `--json` 即可获得机器可读的输出 —— 即 AI agent / MCP server 消费的形式,其结构与 Rust 引擎完全一致(SPEC §3.1):`show`→`[{fn,inferred,direct,unresolved,fs?,hosts?}]`, `where`→`{effect,directly,inherited}`,`callers`→`{of,direct,transitive}`, `map`→`{module:{effects,functions}}`,`diff`→`{changes:[{fn,gained,introduced,inherited,lost,status}]}`, `whatif`→`{of,effect,affected,violations,ok}`,`rewire`→`{dropped:[{caller,no_longer_calls}]}`。 ### `containment` —— 一种并非“分数”的架构质量信号 原始的副作用*计数*是依赖于领域的(一个数据库应用会有大量的 `Db` —— 这并不是缺陷),因此不存在单一的“candor 分数”。但是,跨越各层的边界副作用的**分散程度**确实*是*一种与领域无关的信号:一个重度使用 DB 的应用,如果所有的 `Db` 都位于 `dao` 中,那它的架构就是良好的;而如果 `Db` 弥漫在 `model`、`actions` *以及* `dao` 中,那它就是有泄漏的 —— 无论它执行了多少 DB 操作。 `containment` 正是衡量这一点的 —— 对于每一个*边界*副作用(`Db`/`Net`/`Exec`/`Fs`/`Ipc`),衡量位于其主要层中的比例(`Log`/`Clock` 是环境性的 —— 会被报告,但不计分)。层是根据公共根之后的 package 推断出来的,无需配置: ``` effect contained layers owner ← leaked into Db 49% 4 model (838) ← dao:833, spring:6, actions:1 # ← half the DB is in `model`, not `dao` Exec 100% 1 utils (1) ``` 给定一个**基线**后,它就是一个*棘轮*(ratchet)—— 将情况变**糟**时设为阻断,将情况变**好**时仅作记录: ``` [containment] a boundary effect leaked into a layer it wasn't in: ← exit 1, fail the PR Db → actions ✓ improved — a boundary effect left a layer: ← informational Db ⊘ legacy ``` 它被刻意设计为一种**诊断 + 棘轮工具,而不是单一的评级** —— 其绝对水平是依赖于领域且可被操纵的,但其趋势(某个边界副作用是否泄漏到了新层?)是一个真实的、可执行的质量门禁。在 CI 中将它与 `cargo candor snapshot` 风格的基线配合使用。 ## 模式 | 模式 | 如何操作 | 输出 | |---|---|---| | **audit** (默认) | `gradle run --args=""` | 每个方法的副作用映射 | | **JSON** | 添加 `--json ` | candor JSON 报告 —— 针对每个方法:`inferred`,`direct`,以及 **conformance**(`declared`/`undeclared`/`overdeclared`,从类的注入依赖中投射而来),以便 agent 能够获取 conformance 信息,而不仅仅是诊断结果 | | **regression guard** | `CANDOR_BASELINE= gradle run --args=""` | `AS-EFF-005` + 如果有任何函数相较于快照增加了副作用则 **exit 1** | | **no-ambient** | `CANDOR_NO_AMBIENT=1` (或名称前缀) | 针对直接使用环境权限的 `AS-EFF-004`(应将其路由至注入的协作者) | | **conformance** | `CANDOR_STRICT=1` (或类名前缀) | `AS-EFF-001/002/003` — 类执行了其注入依赖未提供的副作用(或注入了从未使用的依赖) | | **policy** | `CANDOR_POLICY= gradle run --args=""` | `AS-EFF-006/008/009` + **exit 1** — 架构即代码:某方法(在传递意义上)违反了 `deny`/`pure`/`allow`/`forbid` 边界 | | **taint** (建议性) | `CANDOR_TAINT=1` | `AS-EFF-007` — 注入类的副作用(`Exec`/`Fs`/`Db`/`Net`/`Env`/`Ipc`)作用于**由调用者派生**的参数上(命令/路径/SQL 注入,SSRF)。过程内污染数据流;基于启发式算法,永远不会导致 CI 失败 | ### Policy:架构即代码(`CANDOR_POLICY`) 随着模型在局部推理方面变得越来越强大,这种执行方式证明了其存在的价值:模型只是提供建议,而只有掌握了整个效果图的工具才能*阻止 PR*。Policy 文件用于声明不变量;candor-java 会标记任何**传递性**违规(原因可能存在于另一个方法或被本地 diff 隐藏的层中): ``` # .candor/policy deny Net Db Fs domain # the domain layer must reach no I/O — even through a helper pure parse # parsing must be side-effect-free deny Exec # nothing may spawn a subprocess (no scope = whole project) allow Net in billing api.stripe.com # billing may reach the network — but ONLY Stripe allow Exec in build git # the build layer may run subprocesses — but ONLY git allow Fs in config /etc/app # config code may read the filesystem — but ONLY under /etc/app allow Db in billing ledger.* # billing may touch the database — but ONLY the ledger schema forbid domain -> infra # the domain layer must not depend on the infrastructure layer ``` ``` [AS-EFF-006] `app.domain.Checkout.run` performs { Fs }, forbidden by policy (scope `domain`): `deny Net Db Fs domain` [AS-EFF-008] `billing.Pay.leak` reaches { metrics.growthtracker.io } outside the allowlist, forbidden by policy (scope `billing`): `allow Net … api.stripe.com` [AS-EFF-009] `app.domain.Order.place` reaches into a forbidden layer (via `app.infra.Repo.save`), violating policy: `forbid domain -> infra` ``` - **`deny` / `pure`** (`AS-EFF-006`) — 某一层可以做的*内容*。一个方法不必直接执行该副作用;candor 会标记它通过任何被调用方触达该副作用的情况。`pure` 会禁止所有副作用。 - **`allow in `** (`AS-EFF-008`) — 某个副作用可以触达*哪些字面量*,跨越**传递性**表面(字面量通常存在于深层的被调用方中)。对于 `Db` 表,其表面以两种方式输入:SQL 字符串字面量中的表位置标识符,**以及 JPA 的声明** —— 实体上的字面量 `@Table(name = "users")` 加上 repository 的泛型签名(`extends CrudRepository`)会将 `users` 带入每个 Spring-Data 调用的 `tables` 中,无论何处都不需要 SQL 字符串(光秃秃的 `@Entity` 依赖于命名策略且不会提供任何信息 —— 绝不作猜测)。这是模型无法自检的供应链边界。四种副作用带有字面量表面:`Net` 主机(“billing 只能与 Stripe 通信”,通过主机名匹配),`Exec` 命令(“build 只能运行 git”,通过程序 basename 匹配),`Fs` 路径(“config 只能读取 /etc/app”,通过边界处的路径前缀匹配),以及 `Db` 表(“billing 只能触及 `ledger.*`”,通过来自 SQL 字符串字面量的限定表名匹配)。仅认证*可见的*表面 —— 字面量是从携带它的调用中读取的(`ProcessBuilder`/`exec` 程序,`Path.of`/`File`/stream-ctor 路径,scheme-URL/`host:port`/IP host);runtime 计算的值是诚实地不可见的,从不被过度声明(已在真实的 Spring 应用上验证过 —— 提取器获取的是*第一个*参数,因此 `ProcessBuilder("git","clone")` 是 `git`,而 `RandomAccessFile(path,"r")` 的模式永远不会被误认为是路径)。 - **`forbid -> `** (`AS-EFF-009`) — 某一层可以依赖的*对象*。作用域 A 中的方法绝不能*传递性地*触达作用域 B 中的方法(在 call graph 上进行反向可达性查询)。 Scope 通过点分**片段**进行匹配(因此 `domain` 匹配 `app.domain.Svc.handle` 和 `domain_logic` 包,但不匹配 `subdomain`)—— 这与 Rust 实现中的 `scope_matches` 规则相同。设置了但无法读取的 policy 会**响亮地**失败(“policy NOT enforced”),绝不会悄悄变绿。 ### Conformance:依赖注入*就是*一种能力系统 candor 的 Rust 实现通过能力 token(`&Fs`)来声明副作用。Java 的惯用做法是**依赖注入**:一个 bean 被注入的协作者(其字段)就是它所拥有的能力。因此: - **declared(class)** = 其字段类型可以提供的副作用(`RestTemplate` → `Net`,`@Repository` → `Db`,注入的项目服务 → 该服务的副作用)。 - **performed(class)** = 跨越其自身方法的副作用。 - **`AS-EFF-001`** — 执行了*任何*注入依赖都不提供的副作用 → 它伸手获取了环境权限,而不是接收注入。**`AS-EFF-002`** — 注入了一个从未使用的协作者。 **`AS-EFF-003`** — 存在 `Unknown`,无法认证。 在 `conf-sample/` 上:`GoodService`(持有并使用 `RestTemplate`)是符合规范的;`LeakyService` 在没有 Fs 依赖的情况下调用了环境权限 `Files.readString` → `AS-EFF-001`;`IdleService` 注入了一个从未调用过的 `RestTemplate` → `AS-EFF-002`。这项检查是由一个类的签名(其依赖项)告诉你它的副作用表面 —— 这正是 candor 的核心论点,只是采用了 Java 的颗粒度。 ### CI regression guard(阻力最小的采用方式) 无需声明,无需重构 —— 快照记录副作用表面,提交它,并让增加了副作用的 PR 失败: ``` # 在已知的良好构建上执行一次 —— 提交快照: gradle run --args="build/classes/java/main --json .candor/baseline.json" # 在 CI 中: CANDOR_BASELINE=.candor/baseline.json gradle run --args="build/classes/java/main" # 如果某个函数新增了 effect,则以非零值退出(AS-EFF-005);缺失/损坏的 baseline # 会发出显著警报失败(“guard is NOT active”),而不是静默通过。 ``` ### 尝试示例 ## 工作原理 ASM 将每个 `.class` 读取为节点树;对于每个方法,每个已解析的调用(`MethodInsnNode`)都会根据其目标的 class + 方法名进行分类;对项目方法的调用成为 call graph 的边;一个不动点运算将所有被调用者的副作用合并到调用者中。与 Rust 引擎采用相同的架构,这是刻意为之的 —— 并且它沿着相同的路径成长:`Unknown`(§4 信任契约),针对项目类型的 CHA,Spring 的声明性表面,policy 门禁,以及只读查询都已加入(如上所述)。 ## 许可证 根据您的选择,双重许可于 [MIT](LICENSE-MIT) 或 [Apache-2.0](LICENSE-APACHE)。
标签:Java虚拟机, JS文件枚举, 后台面板检测, 域名枚举, 架构规范, 错误基检测, 静态代码分析