Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package example;

import base.RuleSample;
import base.RuleSet;

@RuleSet("example/JoinTagUnion.yaml")
public abstract class JoinTagUnion implements RuleSample {

static Object srcA() { return null; }
static Object srcB() { return null; }
static void sinkX(Object v) {}
static void sinkY(Object v) {}

/** Positive: source A into sink X. */
static class PositiveAtoX extends JoinTagUnion {
@Override public void entrypoint() {
Object a = srcA();
sinkX(a);
}
}

/** Positive: source B into sink Y (second tag-expanded source, second sink). */
static class PositiveBtoY extends JoinTagUnion {
@Override public void entrypoint() {
Object b = srcB();
sinkY(b);
}
}

/** Positive: source A into sink Y (cross-path; proves the tag union is not source-paired). */
static class PositiveAtoY extends JoinTagUnion {
@Override public void entrypoint() {
Object v = srcA();
sinkY(v);
}
}

/** Positive: source B into sink X (cross-path; proves the tag union is not source-paired). */
static class PositiveBtoX extends JoinTagUnion {
@Override public void entrypoint() {
Object v = srcB();
sinkX(v);
}
}

/** Negative: untainted value into a sink. */
static class NegativeCleanIntoSink extends JoinTagUnion {
@Override public void entrypoint() {
Object c = "safe";
sinkX(c);
}
}

/** Negative: tainted source never reaches a sink. */
static class NegativeNoSink extends JoinTagUnion {
@Override public void entrypoint() {
Object a = srcA();
System.out.println(a);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
rules:
- id: tag-source-a
options: { lib: true }
tags: [demo-untrusted]
severity: NOTE
message: source a
languages: [java]
patterns:
- pattern: $V = srcA()

- id: tag-source-b
options: { lib: true }
tags: [demo-untrusted]
severity: NOTE
message: source b
languages: [java]
patterns:
- pattern: $V = srcB()

- id: tag-sink-x
options: { lib: true }
severity: NOTE
message: sink x
languages: [java]
patterns:
- pattern: sinkX($V)

- id: tag-sink-y
options: { lib: true }
severity: NOTE
message: sink y
languages: [java]
patterns:
- pattern: sinkY($V)

- id: join-tag-union
severity: ERROR
message: Tag union across two sources into two sinks
languages: [java]
mode: join
join:
refs:
- tag: demo-untrusted
as: untrusted
- rule: tag-sink-x
as: sinkx
- rule: tag-sink-y
as: sinky
on:
- 'untrusted.$V -> sinkx.$V'
- 'untrusted.$V -> sinky.$V'
Original file line number Diff line number Diff line change
Expand Up @@ -224,11 +224,8 @@ class JoinRuleWithNoOperations : UnsupportedFeatureBlockingMessage() {
}

class JoinRuleWithChainedOperations : UnsupportedFeatureBlockingMessage() {
override val message: String = "Join rule with chained operations is not supported; only a single join condition is allowed"
}

class JoinRuleWithMultipleDistinctRightItems : UnsupportedFeatureBlockingMessage() {
override val message: String = "Join rule references multiple distinct right-hand rules; only a single right-hand rule is supported"
override val message: String =
"Join rule chains an alias as both a source and a sink; intermediate (chained) nodes are not supported"
}

class JoinOnTaintRuleWithNonEmptySources : RuleIssueBlockingMessage() {
Expand All @@ -251,6 +248,36 @@ class JoinIsImpossibleNoLabelFound(label: String) : RuleIssueBlockingMessage() {
override val message: String = "Join is impossible: taint label '$label' required by the join condition was not found in the left-hand rule"
}

class EmptyTagExpansion(tag: String) : RuleIssueBlockingMessage() {
override val message: String =
"Join ref targets tag '$tag', but no rule declares that tag"
}

class JoinRefMissingTarget : RuleIssueBlockingMessage() {
override val message: String =
"Join ref must specify exactly one of 'rule' or 'tag', but neither was given"
}

class JoinRefAmbiguousTarget : RuleIssueBlockingMessage() {
override val message: String =
"Join ref must specify exactly one of 'rule' or 'tag', but both were given"
}

class JoinRefToUnsupportedRuleKind(ruleId: String) : RuleIssueBlockingMessage() {
override val message: String =
"Join ref resolves to '$ruleId', which is a join rule; only search/taint rules may be wired into a join"
}

class JoinRefDuplicateAlias(alias: String) : RuleIssueBlockingMessage() {
override val message: String =
"Join alias '$alias' is declared by more than one ref; each ref must use a distinct 'as' alias (use a single 'tag' ref to union several rules under one alias)"
}

class JoinAliasMetavarConflict(alias: String) : RuleIssueBlockingMessage() {
override val message: String =
"Join alias '$alias' is referenced with conflicting metavariables across 'on' conditions; an alias must use a single metavariable on each side"
}

class FailedToConvertToTaintRule(causeMessage: String?) : InternalWarningBlockingMessage() {
override val message: String = "Failed to convert automata to a taint rule: ${causeMessage ?: "unknown error"}"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ class SemgrepRuleLoader(
)

private val registeredRules = hashMapOf<String, RegisteredRule>()
private val tagIndex = hashMapOf<String, MutableList<String>>()

fun registerRuleSet(
ruleSetText: String,
Expand Down Expand Up @@ -95,6 +96,15 @@ class SemgrepRuleLoader(
semgrepFileTrace.info("Register ${supportedRules.size} rules")
}

private fun buildTagIndex() {
tagIndex.clear()
for (registered in registeredRules.values) {
for (tag in registered.rule.tags) {
tagIndex.getOrPut(tag, ::mutableListOf).add(registered.ruleId)
}
}
}

private fun registerRule(rule: RegisteredRule) {
if (rule.ruleId in registeredRules) {
rule.ruleTrace.stepTrace(Step.LOAD_RULESET)
Expand All @@ -119,6 +129,8 @@ class SemgrepRuleLoader(
registeredRules.values.toList()
.forEach { parseRule(it, forceLibraryMode = false) }

buildTagIndex()

resolveRuleOverrides()

parsedRules.values
Expand Down Expand Up @@ -385,14 +397,18 @@ class SemgrepRuleLoader(
}

private fun buildJoinRule(rule: JoinRule<*>, trace: SemgrepRuleLoadStepTrace): TaintAutomataJoinRule? {
val items = hashMapOf<String, TaintAutomataJoinRuleItem>()
val items = hashMapOf<String, MutableList<TaintAutomataJoinRuleItem>>()
val itemRenames = hashMapOf<String, List<Pair<MetavarAtom, MetavarAtom>>>()

val strategy = strategyFor(rule.info) ?: return null

for (ref in rule.refs) {
val refId = resolveRefRuleId(ref.rule, rule.info.pathInfo.ruleRelativePath)
val itemAutomata = resolveBuiltRuleWrtOverrides(refId, trace, hashSetOf())
if (ref.`as` in items) {
trace.error(JoinRefDuplicateAlias(ref.`as`))
return null
}

val refIds = resolveRefTargets(ref, rule.info.pathInfo.ruleRelativePath, trace)
?: return null

val renames = ref.renames.map {
Expand All @@ -401,7 +417,16 @@ class SemgrepRuleLoader(
Pair(from, to)
}

items[ref.`as`] = TaintAutomataJoinRuleItem(itemAutomata.info.ruleId, itemAutomata.rule)
val aliasItems = items.getOrPut(ref.`as`, ::mutableListOf)
for (refId in refIds) {
if (parsedRules[refId] is JoinRule<*>) {
trace.error(JoinRefToUnsupportedRuleKind(refId))
return null
}
val itemAutomata = resolveBuiltRuleWrtOverrides(refId, trace, hashSetOf())
?: return null
aliasItems += TaintAutomataJoinRuleItem(itemAutomata.info.ruleId, itemAutomata.rule)
}
itemRenames[ref.`as`] = renames
}

Expand All @@ -422,7 +447,31 @@ class SemgrepRuleLoader(
return null
}

return TaintAutomataJoinRule(items, operations)
return TaintAutomataJoinRule(items.mapValues { it.value.toList() }, operations)
}

private fun resolveRefTargets(
ref: SemgrepYamlJoinRuleRef,
ruleRelativePath: Path,
trace: SemgrepRuleLoadStepTrace
): List<String>? {
val hasRule = ref.rule != null
val hasTag = ref.tag != null
if (hasRule == hasTag) {
trace.error(if (hasRule) JoinRefAmbiguousTarget() else JoinRefMissingTarget())
return null
}

if (hasRule) {
return listOf(resolveRefRuleId(ref.rule!!, ruleRelativePath))
}

val matched = tagIndex[ref.tag]
if (matched.isNullOrEmpty()) {
trace.error(EmptyTagExpansion(ref.tag!!))
return null
}
return matched.distinct().sorted()
}

private fun LanguageStrategy<*, *>.parseJoinMetaVarWithRenames(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ data class SemgrepYamlRuleSet(
@Serializable
data class SemgrepYamlRule(
val id: String,
val tags: List<String> = emptyList(),
val languages: List<String>? = null,
val pattern: String? = null,
val mode: String? = null,
Expand Down Expand Up @@ -56,7 +57,8 @@ data class SemgrepYamlJoinRule(

@Serializable
data class SemgrepYamlJoinRuleRef(
val rule: String,
val rule: String? = null,
val tag: String? = null,
@SerialName("as")
val `as`: String,
val renames: List<SemgrepYamlJoinRuleRefRenames> = emptyList()
Expand Down
Loading
Loading