Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build-client.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ ROOT_DIR=$(cd "$(dirname "$0")" && pwd)
LOGGER_FILE="$ROOT_DIR/src/util/logger.ts"
PACKAGE_FILE="$ROOT_DIR/package.json"
STARTER_FILE="$ROOT_DIR/src/interface/starter.ts"
SNAPSHOT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/engine-client-build.XXXXXX")
SNAPSHOT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/yasa2-client-build.XXXXXX")
LOGGER_SNAPSHOT="$SNAPSHOT_DIR/logger.ts"
PACKAGE_SNAPSHOT="$SNAPSHOT_DIR/package.json"
STARTER_SNAPSHOT="$SNAPSHOT_DIR/starter.ts"
Expand Down
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,14 +98,17 @@
"test-clone-util": "npx mocha --require tsx/cjs test/rules-basic-handler/test-clone-util.ts",
"test-sink-util-new-expr": "npx mocha --require tsx/cjs test/rules-basic-handler/test-sink-util-new-expr.ts",
"test-sink-util-go-interface-passthrough": "npx mocha --require tsx/cjs test/rules-basic-handler/test-sink-util-go-interface-passthrough.ts",
"test-sink-util-go-qid-fallback": "npx mocha --require tsx/cjs test/rules-basic-handler/test-sink-util-go-qid-fallback.ts",
"test-taint-output-strategy": "npx mocha --require tsx/cjs test/rules-basic-handler/test-taint-output-strategy.ts",
"test-go-dynamic-key-taint": "npx mocha --require tsx/cjs test/rules-basic-handler/test-go-dynamic-key-taint.ts",
"test-go-json-unmarshal-field-taint": "npx mocha --require tsx/cjs test/rules-basic-handler/test-go-json-unmarshal-field-taint.ts",
"test-go-short-var-callback-taint": "npx mocha --require tsx/cjs test/rules-basic-handler/test-go-short-var-callback-taint.ts",
"test-go-callsite-limit-memory-guard": "npx mocha --require tsx/cjs test/go/test-go-callsite-limit-memory-guard.ts",
"test-go-callsite-limit-integration": "npx mocha --require tsx/cjs test/go/test-go-callsite-limit-integration.ts",
"test-php-checker": "npx mocha --require tsx/cjs test/php/test-php-checker.ts",
"test-php": "npx mocha --require tsx/cjs test/php/test-php-benchmark.ts",
"update-php-expect": "YASA_UPDATE_PHP_BENCHMARK_EXPECT=1 npm run test-php && cp test/php/expect/phpbenchmark-expect.json.candidate test/php/expect/phpbenchmark-expect.json && rm test/php/expect/phpbenchmark-expect.json.candidate && npm run test-php",
"test-all": "npm run test-callargs && npm run test-fastapi-backgroundtasks && npm run test-match-field && npm run test-entrypoint-deadline-budget && npm run test-entrypoint-deadline-scheduler && npm run test-clone-util && npm run test-sink-util-new-expr && npm run test-taint-output-strategy && npm run test-go-dynamic-key-taint && npm run test-js && npm run test-java && npm run test-sarif-codeflow-quality && npm run test-go && npm run test-python && npm run test-callchain && npm run test-php",
"test-all": "npm run test-callargs && npm run test-fastapi-backgroundtasks && npm run test-match-field && npm run test-entrypoint-deadline-budget && npm run test-entrypoint-deadline-scheduler && npm run test-clone-util && npm run test-sink-util-new-expr && npm run test-taint-output-strategy && npm run test-go-dynamic-key-taint && npm run test-go-callsite-limit-memory-guard && npm run test-go-callsite-limit-integration && npm run test-sink-util-go-qid-fallback && npm run test-js && npm run test-java && npm run test-sarif-codeflow-quality && npm run test-go && npm run test-python && npm run test-callchain && npm run test-php",
"build": "bash build.sh",
"prepare-better-sqlite3-native": "bash scripts/download-better-sqlite3-native.sh",
"lint": "eslint .",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"id": "SANITIZER_2",
"sanitizerType": "FunctionCallSanitizer",
"sanitizerScenario": "SANITIZER.VALIDATE_BY_FUNCTIONCALL",
"calleeType": "com.example.security.util.CommandParameterChecker",
"calleeType": "com.alipay.common.security.util.secutils.cmd.CmdParameterChecker",
"fsig": "checkCmdParameter",
"args": [
"0"
Expand All @@ -26,7 +26,7 @@
"id": "SANITIZER_3",
"sanitizerType": "FunctionCallSanitizer",
"sanitizerScenario": "SANITIZER.CALLSTACK_HAS_FUNCTIONCALL",
"calleeType": "com.example.security.util.CommandChecker",
"calleeType": "com.alipay.common.security.util.secutils.cmd.AlipayCmdChecker",
"fsig": "hookStart",
"args": [
"0"
Expand Down
57 changes: 57 additions & 0 deletions scripts/regtest-run-case.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
#!/usr/bin/env bash
# 回归用例标准执行脚本(源码形式 tsx + src/main.ts),参数从用例 .config 读取,不自行改参数。
# 用法: ENGINE_DIR=<engine path> scripts/regtest-run-case.sh <case-name> <report-dir> [testcase-root]
# ENGINE_DIR 默认为本脚本所在仓库根(ours)。
# 严格串行:同一 engine 目录禁止并行;调用方负责不并行起任务。
set -uo pipefail

CASE="${1:?case name required}"
REPORT="${2:?report dir required}"
ROOT="${3:-/Users/ariel/code/yasa-reformat/test/yasa2-bench/testcase/python}"

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
ENGINE_DIR="${ENGINE_DIR:-$(cd "$SCRIPT_DIR/.." && pwd)}"
cd "$ENGINE_DIR"

CASE_DIR="$ROOT/$CASE"
# mpc_server 目录内部 case 名是 mcp_server,特例处理
if [[ "$CASE" == "mpc_server" ]]; then
CFG_NAME="mcp_server"
SOURCE_PATH="$CASE_DIR/mcp_server"
else
CFG_NAME="$CASE"
SOURCE_PATH="$CASE_DIR/$CASE"
fi

CONFIG_FILE="$CASE_DIR/$CFG_NAME.config"
[[ -f "$CONFIG_FILE" ]] || { echo "missing $CONFIG_FILE" >&2; exit 2; }

CHECKER_PACKS=$(python -c "import json;d=json.load(open('$CONFIG_FILE'));print(','.join(d['checkerPackIds']))")
ANALYZER=$(python -c "import json;d=json.load(open('$CONFIG_FILE'));print(d.get('analyzer',''))")
LANGUAGE=$(python -c "import json;d=json.load(open('$CONFIG_FILE'));print(d.get('language',''))")
RULE_CONFIG_REL=$(python -c "import json;d=json.load(open('$CONFIG_FILE'));print(d.get('ruleConfigFile',''))")
RULE_CONFIG="$CASE_DIR/${RULE_CONFIG_REL#./}"

NODE_OPTS=$(python -c "import json;d=json.load(open('$CONFIG_FILE'));print(d.get('nodeOptions',''))")
[[ -n "$NODE_OPTS" ]] && export NODE_OPTIONS="$NODE_OPTS"

[[ -d "$SOURCE_PATH" ]] || { echo "missing sourcePath $SOURCE_PATH" >&2; exit 2; }

rm -rf "$REPORT"; mkdir -p "$REPORT"

ARGS=(--sourcePath "$SOURCE_PATH" --checkerPackIds "$CHECKER_PACKS" --language "$LANGUAGE" --ruleConfigFile "$RULE_CONFIG" --report "$REPORT")
[[ -n "$ANALYZER" ]] && ARGS+=(--analyzer "$ANALYZER")

# 留痕:完整命令写进 report/cmd.log
{
echo "# ENGINE_DIR=$ENGINE_DIR"
echo "# HEAD=$(git rev-parse --short HEAD 2>/dev/null)"
echo "# NODE_OPTIONS=${NODE_OPTIONS:-}"
echo "node ./node_modules/tsx/dist/cli.mjs ./src/main.ts \\"
for a in "${ARGS[@]}"; do echo " $a \\"; done
echo
} > "$REPORT/cmd.log"

echo "[regtest] ENGINE=$ENGINE_DIR CASE=$CASE REPORT=$REPORT"
/usr/bin/time -l node ./node_modules/tsx/dist/cli.mjs ./src/main.ts "${ARGS[@]}" 1>/dev/null 2>"$REPORT/time.log"
echo "[regtest] exit=$?"
54 changes: 39 additions & 15 deletions src/checker/common/output/taint-output-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type { IResultManager } from '../../../engine/analyzer/common/result-manager'
import type { IConfig } from '../../../config'
import type { TaintFinding } from '../../../engine/analyzer/common/common-types'
import type { SarifLocation } from '../../../engine/analyzer/common/sarif'

const _ = require('lodash')
const path = require('path')
Expand All @@ -16,8 +17,7 @@ const logger = require('../../../util/logger')(__filename)
const {
registerDedupFunction,
} = require('../../../engine/analyzer/common/entrypoint/merge-coordinator') as typeof import('../../../engine/analyzer/common/entrypoint/merge-coordinator')

type SarifLocation = { physicalLocation?: Record<string, unknown>; [key: string]: unknown }
const { getJavaFindingSignature } = require('../../taint/java/java-finding-candidate') as typeof import('../../taint/java/java-finding-candidate')

const {
prepareLocation,
Expand Down Expand Up @@ -265,7 +265,7 @@ function extractSinkKey(finding: TracePreserveFinding): string | null {
}

type JavaTraceGroup = { call: string; argPass: string }
type JavaFindingProjection = { source: string; sink: string; groups: JavaTraceGroup[] }
type JavaFindingProjection = { source: string; sink: string; sinkAttribute: string; groups: JavaTraceGroup[] }

function traceItemIdentity(item: TaintTraceItem): string | null {
const file = getTraceFileKey(item)
Expand All @@ -282,6 +282,9 @@ function getJavaFindingProjection(finding: TracePreserveFinding): JavaFindingPro
const source = extractSourceKey(finding)
const sink = extractSinkKey(finding)
if (!source || source.includes(':-1:-1') || !sink || sink.includes(':-1:-1')) return null
// sinkAttribute 纳入投影键,避免不同 attribute 的同类 finding 被误去重
const attrArr = (finding as any).sinkAttribute as string[] | undefined
const sinkAttribute = Array.isArray(attrArr) ? attrArr.slice().sort().join(',') : ''
const groups: JavaTraceGroup[] = []
for (let i = 0; i < finding.trace.length; i++) {
const item = finding.trace[i]
Expand All @@ -293,12 +296,12 @@ function getJavaFindingProjection(finding: TracePreserveFinding): JavaFindingPro
if (call && argPass) groups.push({ call, argPass })
i++
}
return { source, sink, groups }
return { source, sink, sinkAttribute, groups }
}

function getJavaProjectionKey(projection: JavaFindingProjection): string {
const groups = projection.groups.map((group) => `${group.call}${group.argPass}`).join('')
return `${projection.source}${projection.sink}${groups}`
return `${projection.source}${projection.sink}${projection.sinkAttribute}${groups}`
}

const javaProjectionIndexes = new WeakMap<TaintFinding[], Map<string, TaintFinding>>()
Expand All @@ -316,15 +319,21 @@ function getJavaProjectionIndex(category: TaintFinding[]): Map<string, TaintFind
}

function isDuplicateJavaFinding(category: TaintFinding[], finding: TaintFinding): boolean {
const projection = getJavaFindingProjection(finding as TracePreserveFinding)
if (!projection) return false
return getJavaProjectionIndex(category).has(getJavaProjectionKey(projection))
const signature = getJavaFindingSignature(finding)
if (!signature) return false
for (const issue of category) {
if (getJavaFindingSignature(issue) === signature) return true
}
return false
}

function rememberJavaFinding(category: TaintFinding[], finding: TaintFinding): void {
const signature = getJavaFindingSignature(finding)
if (!signature) return
// 保持统一 finding 签名索引的建立副作用,实际查重使用同一 helper。
const projection = getJavaFindingProjection(finding as TracePreserveFinding)
if (!projection) return
getJavaProjectionIndex(category).set(getJavaProjectionKey(projection), finding)
if (projection) getJavaProjectionIndex(category).set(getJavaProjectionKey(projection), finding)
void signature
}

function invalidateJavaProjectionIndex(category: TaintFinding[]): void {
Expand Down Expand Up @@ -383,7 +392,7 @@ class TaintOutputStrategy extends OutputStrategy {
* @param config
* @param printf
*/
outputFindings(resultManager: IResultManager, outputFilePath: string, config: IConfig, printf: any): void {
outputFindings(resultManager: IResultManager, outputFilePath: string, config: IConfig, printf: any): any {
const outputStartedAt = Date.now()
let reportFilePath
if (resultManager) {
Expand Down Expand Up @@ -425,8 +434,10 @@ class TaintOutputStrategy extends OutputStrategy {
logger.info(`[outputFindings] strategy=taintflow phase=write elapsed=${Date.now() - writeStartedAt}ms total=${Date.now() - outputStartedAt}ms`)
// for taint flow checker, output result to console at the same time
logger.info(`report is write to ${reportFilePath}`)
return results
}
}
return undefined
}

/**
Expand All @@ -439,14 +450,27 @@ class TaintOutputStrategy extends OutputStrategy {
if (!finding) return false
let category: TaintFinding[] | undefined
try {
category = resultManager?.findings[TaintOutputStrategy.outputStrategyId] as TaintFinding[] | undefined
// 跨入口 dedup:dedupIndex(已 snapshot 的历史 finding)+ findings(当前批次 live finding)
const dedupCategory = resultManager?.dedupIndex?.[TaintOutputStrategy.outputStrategyId] as TaintFinding[] | undefined
const currentCategory = resultManager?.findings?.[TaintOutputStrategy.outputStrategyId] as TaintFinding[] | undefined
if (!dedupCategory && !currentCategory) return true
if (!dedupCategory) {
category = currentCategory
} else if (!currentCategory) {
category = dedupCategory
} else {
category = [...dedupCategory, ...currentCategory]
}
if (!category) return true
if (isDuplicateJavaFinding(category as TaintFinding[], finding)) return false
// 依赖 trace 形态的折叠判据只比较原始链路节点,避免输出辅助节点改变去重结果。
// 非 callstack-only 输出下 trace 通常不含 `_synthetic` 标记,过滤后与原数组等价。
const findingTraceNoSynthetic = filterOutSyntheticSteps(getDedupTrace(finding))
for (let i = 0; i < category.length; i++) {
const issue = category[i]
const issueProjection = getJavaFindingProjection(issue as TracePreserveFinding)
const findingProjection = getJavaFindingProjection(finding as TracePreserveFinding)
if (issueProjection && findingProjection && getJavaProjectionKey(issueProjection) !== getJavaProjectionKey(findingProjection)) continue
if (
issue.line === finding.line &&
isNodeEqual(issue.node, finding.node) &&
Expand Down Expand Up @@ -556,7 +580,7 @@ class TaintOutputStrategy extends OutputStrategy {
uri,
snippetText,
nodeHash,
affectedNodeName,
affectedNodeName
),
key
)
Expand All @@ -573,7 +597,7 @@ class TaintOutputStrategy extends OutputStrategy {
'egg controller',
item.str,
nodeHash,
affectedNodeName,
affectedNodeName
),
key
)
Expand Down Expand Up @@ -605,7 +629,7 @@ class TaintOutputStrategy extends OutputStrategy {
trace,
location,
finding.matchedSanitizerTags,
callstackElements,
callstackElements
)
)
})
Expand Down
15 changes: 10 additions & 5 deletions src/checker/taint/common-kit/sink-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,11 @@ const GO_INTERFACE_PASSTHROUGH_MAX_IMPLEMENTERS = 8
* 注意:本函数只在 Go 路径(由调用方传入 analyzer + declType 才会触发)启用,
* Java 共用的 checkInvocationMatchSink 不受影响。
*
* Go 分析流程把层级信息写入 analyzer.classHierarchyMap,而子类型查询由
* analyzer.typeResolver.findSubTypes 提供,因此 helper 同时读取这两个对象。
* Bug 1 修复(W4 实测):Go 路径下 classHierarchyMap 由 checker triggerAtStartOfAnalyze 调
* `analyzer.typeResolver.findClassHierarchy` 后写入到 `analyzer.classHierarchyMap`(独立对象,
* size 实测 42),而 `analyzer.typeResolver.classHierarchyMap` 始终是默认空 Map(size 0)。
* helper 入参改为接受 analyzer,从 analyzer 读 classHierarchyMap,从 analyzer.typeResolver
* 读 findSubTypes 方法。
*/
function tryMatchSinkGoInterfacePassthrough(
declType: string,
Expand Down Expand Up @@ -321,8 +324,8 @@ function matchSinkAtFuncCallWithCalleeType(
}

/**
* 当 fclos.rtype 缺失但 fclos.qid 编码 receiver 类型时(如 Go 全局变量 method call),
* 按 qid 后缀匹配 calleeType 与 fsig,并忽略 qid 中的 `<instance_*>` 标记。
* W14f: 当 fclos.rtype 缺失但 fclos.qid 编码 receiver 类型时(如 Go 全局变量 method call),
* 按 qid 后缀匹配 calleeType 与 fsig。先 strip qid 中的 `<instance_*>` 标记。
* @param qid fclos.qid
* @param calleeType sink rule calleeType(可能带 `*` 前缀)
* @param calleeTypeBase calleeType 去 `*` 后的形式
Expand Down Expand Up @@ -379,10 +382,12 @@ function matchEmptyCalleeTypeInstanceMethod(
if (propertyName !== methodName) return false

const cleanQid = typeof fclos.qid === 'string' ? fclos.qid.replace(/<instance_[^>]*>/g, '') : ''
// Go qid 用 `/` 分隔包路径,rule.fsig 用 `.`,统一归一化后再做 endsWith
const normalizedQid = cleanQid.replace(/\//g, '.')
return (
matchReceiverTypeName(AstUtilSinkUtil.prettyPrint(fclos.object?.rtype?.definiteType), typeName) ||
matchReceiverTypeName(AstUtilSinkUtil.prettyPrint(fclos.rtype?.definiteType), typeName) ||
cleanQid.endsWith(`.${rule.fsig}`)
normalizedQid.endsWith(`.${rule.fsig}`)
)
}

Expand Down
65 changes: 65 additions & 0 deletions src/checker/taint/java/callee-entry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
const QidUnifyUtil = require('../../../util/qid-unify-util') as { qidUnifyByRemoveAngleAndPrefix(value: string): string }

export type CalleeEntryLocation = {
sourcefile: string
start: { line: number; column?: number }
end: { line: number; column?: number }
}

type NodeLocation = {
sourcefile?: string
start?: { line?: number; column?: number }
end?: { line?: number; column?: number }
}

type CalleeEntryNode = {
loc?: NodeLocation
_meta?: { nodehash?: unknown }
id?: { name?: string; loc?: NodeLocation }
body?: { loc?: NodeLocation }
[key: string]: unknown
}

type CalleeEntryFrame = {
fname?: string
qid?: string
ast?: { node?: CalleeEntryNode }
}

export type CalleeEntry = {
file: string
line: number
loc: CalleeEntryLocation
node: CalleeEntryNode
name: string
}

function validLocation(location: NodeLocation | undefined): { file: string; line: number; column?: number } | undefined {
const file = location?.sourcefile
const line = location?.start?.line
if (typeof file !== 'string' || file.length === 0 || typeof line !== 'number') return undefined
return { file, line, column: location?.start?.column }
}

/**
* 统一 callee-entry 展示位置:方法标识符、方法体、函数节点依次回退,并压缩为单行。
* 节点显式复制 frame nodeHash,避免位置包装丢失调用栈身份。
*/
export function getCalleeEntry(frame: unknown): CalleeEntry | undefined {
if (!frame || typeof frame !== 'object') return undefined
const candidate = frame as CalleeEntryFrame
const node = candidate.ast?.node
if (!node) return undefined
const selected = validLocation(node.id?.loc) ?? validLocation(node.body?.loc) ?? validLocation(node.loc)
if (!selected) return undefined
const nodehash = node._meta?.nodehash
const loc: CalleeEntryLocation = {
sourcefile: selected.file,
start: { line: selected.line, column: selected.column ?? 0 },
end: { line: selected.line, column: selected.column ?? 0 },
}
const wrappedNode: CalleeEntryNode = { ...node, loc, _meta: { ...node._meta, nodehash } }
const rawName = node.id?.name || candidate.fname || candidate.qid || '<bridge>'
const name = QidUnifyUtil.qidUnifyByRemoveAngleAndPrefix(rawName) || rawName
return { file: selected.file, line: selected.line, loc, node: wrappedNode, name }
}
Loading
Loading