Skip to content

Added .with[Optional]NamedParameter and tests - #527

Open
san-zrl wants to merge 1 commit into
mainfrom
feat/named-and-optional-parameters
Open

Added .with[Optional]NamedParameter and tests#527
san-zrl wants to merge 1 commit into
mainfrom
feat/named-and-optional-parameters

Conversation

@san-zrl

@san-zrl san-zrl commented Aug 28, 2026

Copy link
Copy Markdown
Contributor

Add with[Optional]NamedMethodParameter to the detection rule DSL

Engine · Python · keyword-argument-aware parameter matching

Motivation

Python callers routinely pass arguments out of order using keyword syntax, and routinely omit optional parameters entirely:

SHA512.new(truncate="256")                                       # data omitted
SHA512.new(truncate="256", data=b"msg")                          # reordered
PBKDF2HMAC(iterations=480000, algorithm=SHA256(), salt=s, length=32)  # fully reordered

The existing withMethodParameter(type) method is purely positional: it reads argument n for parameter n and requires an exact argument count enforced by MethodMatcher. It cannot handle reordered arguments, omitted optional parameters, or the keyword-argument call style that is idiomatic in Python cryptography APIs.

Description

Add support for named and optional named method parameters in detection rules for the Python engine.

New builder methods

Two new methods are added to the DetectionRuleBuilder fluent API:

Method Signature Behaviour
withNamedMethodParameter (String name, String type) Declares a required named parameter. The engine resolves it first by keyword-argument name, then falls back to the positional index if the argument at that index carries no keyword marker. If the parameter is absent or its type does not match the declaration, the rule does not fire.
withOptionalNamedMethodParameter (String name, String type) Declares an optional named parameter. Resolution follows the same keyword-name → positional-fallback order. If the parameter is absent the rule still fires; if it is present but the type does not match, that single parameter is silently skipped.

The pre-existing withMethodParameter and withMethodParameterMatchExactType are fully backward-compatible: rules that use only positional parameters are unaffected — the builder takes the same code path as before and the MethodMatcher is constructed with the same type list as it always was. The only addition is a guard that throws IllegalStateException if a positional-parameter call is made after a named-parameter call, enforcing Python's calling-convention order at construction time.

Ordering constraints (enforced at construction time):

  • All withMethodParameter / withMethodParameterMatchExactType calls must precede any withNamedMethodParameter / withOptionalNamedMethodParameter call. Violating this throws IllegalStateException.
  • All required named parameters (withNamedMethodParameter) must precede optional ones (withOptionalNamedMethodParameter). Violating this also throws IllegalStateException.

Example rule definition:

new DetectionRuleBuilder<Tree>()
    .createDetectionRule()
    .forObjectTypes("test.module.Foo")
    .forMethods("f")
    .shouldBeDetectedAs(new ValueActionFactory<>("f"))
    .withMethodParameter("str")                           // a — positional, required
    .withNamedMethodParameter("b", "int")                 // b — named, required
    .withOptionalNamedMethodParameter("c", "str")         // c — named, optional
    .shouldBeDetectedAs(new AlgorithmParameterFactory<>(AlgorithmParameter.Kind.ANY))
    .asChildOfParameterWithId(2)
    .buildForContext(new DigestContext())
    .inBundle(() -> "Test")
    .withoutDependingDetectionRules();

Engine changes (PythonDetectionEngine)

When a rule contains at least one named parameter:

  1. The MethodMatcher is constructed without a type list so the matcher accepts any arity call and delegates structural validation to the extraction loop.
  2. Before emitting any detection, a set of pre-flight gates runs:
    • Unpacking gate — rejects calls with *args / **kwargs arguments whose contents cannot be statically inspected.
    • Arity gate — rejects calls where arguments.size() < minArgs (minArgs = positional params + required named params).
    • Required-named-present gate — rejects calls where a required named parameter cannot be resolved (neither by keyword name nor by positional fallback).
    • Positional type-check gate — rejects calls where a positional parameter's resolved type does not match its declaration.
  3. A new helper findArgumentByKeyword(name, positionalIndex, arguments) resolves each named parameter: keyword-name match first, positional fallback second (only when the argument at that index carries no keyword marker itself).
  4. The repeated extraction logic is extracted into processParameterExpression to avoid duplication between the positional and named paths.
  5. checkSymbolTraceState is extracted from checkCurrentIndexState so the symbol-trace check can be reused on the named-parameter path without re-running the arity guard.

Test: NamedParametersTest

Located at python/src/test/java/com/ibm/plugin/rules/detection/NamedParametersTest.java.

The test rule models test.module.Foo.f(a: str, b: int, c: str = …) — one positional-required, one named-required, one named-optional parameter.

The optional parameter c (type str) is captured as an AlgorithmParameter child so each test can assert whether it was resolved.

Tests where the rule fires

Test method Python call c child captured Why the rule fires
testAllPositional f("hello", 42, "yes") a at index 0, b via positional fallback at index 1, c via positional fallback at index 2
testCByKeyword f("hello", 42, c="yes") a/b positional, c matched by keyword name "c"
testBCByKeywordCanonicalOrder f("hello", b=42, c="yes") a positional, b and c matched by keyword name in declaration order
testBCByKeywordReordered f("hello", c="yes", b=42) a positional, b and c matched by keyword name regardless of physical argument order
testAllByKeyword f(a="hello", b=42, c="yes") a is a positional rule parameter — engine reads arguments.get(0) = a="hello" and unwraps "hello"; b and c by keyword name
testExtraUnknownKwarg f("hello", 42, "yes", d=0) All required params satisfied; d=0 at index 3 is never referenced and silently ignored
testCAbsentPositional f("hello", 42) a and b satisfied; c is optional and absent — rule still fires, no c child
testCAbsentBByKeyword f("hello", b=42) a positional, b by keyword; c is optional and absent — rule still fires, no c child
testCAbsentAllKeyword f(a="hello", b=42) a at index 0, b by keyword; c is optional and absent — rule still fires, no c child

Tests where the rule does NOT fire

Test method Python call Why the rule does not fire
testNegativeBAbsent f("hello") arguments.size() = 1 < minArgs = 2 — arity gate rejects before any type check
testNegativeWrongTypeA f(42, 42) Positional type-check gate resolves arguments[0] as int, rejects against declared type "str"
testNegativeWrongTypeB f("hello", "42") b found via positional fallback at index 1; named-parameter type check resolves "42" as str, rejects against "int"; because b is required the rule returns early
testNegativeAAbsent f(b=42) arguments.size() = 1 < minArgs = 2 — arity gate rejects (the single argument is keyword b, leaving the positional slot for a empty)
testNegativeDictUnpack f("hello", **d) Unpacking gate detects a non-RegularArgument and rejects the call immediately

Additional test: DetectionRuleBuilderParameterOrderTest

Located at engine/src/test/java/com/ibm/engine/rule/builder/DetectionRuleBuilderParameterOrderTest.java.

Verifies that the ordering constraints are enforced at construction time (positionalAfterNamedThrows, requiredNamedAfterOptionalNamedThrows, requiredBeforeOptionalNamedIsValid) and that a named parameter's keyword name is not silently dropped when .addDependingDetectionRules() is chained (namedParameterWithDependingRulesRetainsKeywordName).

Signed-off-by: san-zrl <san@zurich.ibm.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant