@@ -1990,4 +1990,350 @@ void searchAppVulnerabilities_should_pass_all_standard_filters_to_SDK() throws E
19901990 assertThat (form .getFilterTags ()).isNotNull ();
19911991 assertThat (form .getFilterTags ().size ()).isEqualTo (2 );
19921992 }
1993+
1994+ // ========== Status Filter Verification Tests (mcp-3sy) ==========
1995+ // These tests verify that status filters work correctly after mcp-b9y's unified refactoring
1996+ // eliminated the filterBody object, fixing the bug where status filters were ignored
1997+ // when sessionMetadataName was provided.
1998+
1999+ @ Test
2000+ void searchAppVulnerabilities_should_apply_status_filters_with_sessionMetadataName ()
2001+ throws Exception {
2002+ // Given - Simulate SDK returning only traces matching status filter
2003+ // In reality, SDK filters server-side. We mock that behavior here.
2004+ var mockTraces = new Traces ();
2005+ var traces = new ArrayList <Trace >();
2006+
2007+ // Trace 0: Reported status, branch=main
2008+ Trace trace0 = mock ();
2009+ when (trace0 .getTitle ()).thenReturn ("SQL Injection" );
2010+ when (trace0 .getRule ()).thenReturn ("sql-injection" );
2011+ when (trace0 .getUuid ()).thenReturn ("uuid-reported" );
2012+ when (trace0 .getSeverity ()).thenReturn ("HIGH" );
2013+ when (trace0 .getStatus ()).thenReturn ("REPORTED" );
2014+ when (trace0 .getLastTimeSeen ()).thenReturn (System .currentTimeMillis ());
2015+ when (trace0 .getFirstTimeSeen ()).thenReturn (System .currentTimeMillis () - 86400000L );
2016+
2017+ var sessionMetadata0 = mock (SessionMetadata .class );
2018+ var metadataItem0 = mock (MetadataItem .class );
2019+ lenient ().when (metadataItem0 .getDisplayLabel ()).thenReturn ("branch" );
2020+ lenient ().when (metadataItem0 .getValue ()).thenReturn ("main" );
2021+ lenient ().when (sessionMetadata0 .getMetadata ()).thenReturn (List .of (metadataItem0 ));
2022+ lenient ().when (trace0 .getSessionMetadata ()).thenReturn (List .of (sessionMetadata0 ));
2023+ traces .add (trace0 );
2024+
2025+ // Trace 1: Suspicious status, branch=main
2026+ Trace trace1 = mock ();
2027+ when (trace1 .getTitle ()).thenReturn ("Path Traversal" );
2028+ when (trace1 .getRule ()).thenReturn ("path-traversal" );
2029+ when (trace1 .getUuid ()).thenReturn ("uuid-suspicious" );
2030+ when (trace1 .getSeverity ()).thenReturn ("HIGH" );
2031+ when (trace1 .getStatus ()).thenReturn ("SUSPICIOUS" );
2032+ when (trace1 .getLastTimeSeen ()).thenReturn (System .currentTimeMillis ());
2033+ when (trace1 .getFirstTimeSeen ()).thenReturn (System .currentTimeMillis () - 86400000L );
2034+
2035+ var sessionMetadata1 = mock (SessionMetadata .class );
2036+ var metadataItem1 = mock (MetadataItem .class );
2037+ lenient ().when (metadataItem1 .getDisplayLabel ()).thenReturn ("branch" );
2038+ lenient ().when (metadataItem1 .getValue ()).thenReturn ("main" );
2039+ lenient ().when (sessionMetadata1 .getMetadata ()).thenReturn (List .of (metadataItem1 ));
2040+ lenient ().when (trace1 .getSessionMetadata ()).thenReturn (List .of (sessionMetadata1 ));
2041+ traces .add (trace1 );
2042+
2043+ // Trace 2: Reported status, branch=develop (excluded by session metadata filter)
2044+ Trace trace2 = mock ();
2045+ when (trace2 .getTitle ()).thenReturn ("XSS Reflected" );
2046+ when (trace2 .getRule ()).thenReturn ("xss-reflected" );
2047+ when (trace2 .getUuid ()).thenReturn ("uuid-develop" );
2048+ when (trace2 .getSeverity ()).thenReturn ("MEDIUM" );
2049+ when (trace2 .getStatus ()).thenReturn ("REPORTED" );
2050+ when (trace2 .getLastTimeSeen ()).thenReturn (System .currentTimeMillis ());
2051+ when (trace2 .getFirstTimeSeen ()).thenReturn (System .currentTimeMillis () - 86400000L );
2052+
2053+ var sessionMetadata2 = mock (SessionMetadata .class );
2054+ var metadataItem2 = mock (MetadataItem .class );
2055+ lenient ().when (metadataItem2 .getDisplayLabel ()).thenReturn ("branch" );
2056+ lenient ().when (metadataItem2 .getValue ()).thenReturn ("develop" );
2057+ lenient ().when (sessionMetadata2 .getMetadata ()).thenReturn (List .of (metadataItem2 ));
2058+ lenient ().when (trace2 .getSessionMetadata ()).thenReturn (List .of (sessionMetadata2 ));
2059+ traces .add (trace2 );
2060+
2061+ // Setup mock traces using reflection
2062+ // Note: SDK filters by status server-side, so Fixed/Remediated wouldn't be returned
2063+ try {
2064+ var tracesField = Traces .class .getDeclaredField ("traces" );
2065+ tracesField .setAccessible (true );
2066+ tracesField .set (mockTraces , traces );
2067+
2068+ var countField = Traces .class .getDeclaredField ("count" );
2069+ countField .setAccessible (true );
2070+ countField .set (mockTraces , 3 );
2071+ } catch (Exception e ) {
2072+ throw new RuntimeException ("Failed to create mock Traces" , e );
2073+ }
2074+
2075+ when (mockContrastSDK .getTraces (eq (TEST_ORG_ID ), eq (TEST_APP_ID ), any (TraceFilterForm .class )))
2076+ .thenReturn (mockTraces );
2077+
2078+ // When - call with statuses="Reported,Suspicious" AND sessionMetadataName="branch"
2079+ var result =
2080+ assessService .searchAppVulnerabilities (
2081+ TEST_APP_ID ,
2082+ null , // page
2083+ null , // pageSize
2084+ null , // severities
2085+ "Reported,Suspicious" , // statuses (explicit filter)
2086+ null , // vulnTypes
2087+ null , // environments
2088+ null , // lastSeenAfter
2089+ null , // lastSeenBefore
2090+ null , // vulnTags
2091+ "branch" , // sessionMetadataName
2092+ "main" , // sessionMetadataValue
2093+ null ); // useLatestSession
2094+
2095+ // Then - verify SDK received status filters (CRITICAL verification for mcp-3sy)
2096+ var captor = ArgumentCaptor .forClass (TraceFilterForm .class );
2097+ verify (mockContrastSDK ).getTraces (eq (TEST_ORG_ID ), eq (TEST_APP_ID ), captor .capture ());
2098+
2099+ var form = captor .getValue ();
2100+ assertThat (form .getStatus ())
2101+ .as ("Status filters should be passed to SDK when session filtering is active" )
2102+ .isNotNull ()
2103+ .containsExactlyInAnyOrder ("Reported" , "Suspicious" );
2104+
2105+ // Verify results: trace0 and trace1 match (Reported + Suspicious with branch=main)
2106+ // trace2 is excluded by session metadata filter (branch=develop)
2107+ assertThat (result .items ())
2108+ .as ("Should return traces matching both status and session metadata filters" )
2109+ .hasSize (2 );
2110+ assertThat (result .items ().get (0 ).title ()).isEqualTo ("SQL Injection" );
2111+ assertThat (result .items ().get (1 ).title ()).isEqualTo ("Path Traversal" );
2112+ }
2113+
2114+ @ Test
2115+ void searchAppVulnerabilities_should_apply_status_filters_with_useLatestSession ()
2116+ throws Exception {
2117+ // Given - Simulate SDK returning only Confirmed status traces (SDK filters server-side)
2118+ var mockTraces = new Traces ();
2119+ var traces = new ArrayList <Trace >();
2120+
2121+ // Trace 0: Confirmed status, latest session
2122+ Trace trace0 = mock ();
2123+ when (trace0 .getTitle ()).thenReturn ("Command Injection" );
2124+ when (trace0 .getRule ()).thenReturn ("cmd-injection" );
2125+ when (trace0 .getUuid ()).thenReturn ("uuid-confirmed-latest" );
2126+ when (trace0 .getSeverity ()).thenReturn ("CRITICAL" );
2127+ when (trace0 .getStatus ()).thenReturn ("CONFIRMED" );
2128+ when (trace0 .getLastTimeSeen ()).thenReturn (System .currentTimeMillis ());
2129+ when (trace0 .getFirstTimeSeen ()).thenReturn (System .currentTimeMillis () - 86400000L );
2130+
2131+ var sessionMetadata0 = mock (SessionMetadata .class );
2132+ lenient ().when (sessionMetadata0 .getSessionId ()).thenReturn ("latest-session-id" );
2133+ lenient ().when (trace0 .getSessionMetadata ()).thenReturn (List .of (sessionMetadata0 ));
2134+ traces .add (trace0 );
2135+
2136+ // Trace 1: Confirmed status, old session (excluded by useLatestSession filter)
2137+ Trace trace1 = mock ();
2138+ when (trace1 .getTitle ()).thenReturn ("LDAP Injection" );
2139+ when (trace1 .getRule ()).thenReturn ("ldap-injection" );
2140+ when (trace1 .getUuid ()).thenReturn ("uuid-confirmed-old" );
2141+ when (trace1 .getSeverity ()).thenReturn ("HIGH" );
2142+ when (trace1 .getStatus ()).thenReturn ("CONFIRMED" );
2143+ when (trace1 .getLastTimeSeen ()).thenReturn (System .currentTimeMillis ());
2144+ when (trace1 .getFirstTimeSeen ()).thenReturn (System .currentTimeMillis () - 86400000L );
2145+
2146+ var sessionMetadata1 = mock (SessionMetadata .class );
2147+ lenient ().when (sessionMetadata1 .getSessionId ()).thenReturn ("old-session-id" );
2148+ lenient ().when (trace1 .getSessionMetadata ()).thenReturn (List .of (sessionMetadata1 ));
2149+ traces .add (trace1 );
2150+
2151+ // Setup mock traces using reflection
2152+ try {
2153+ var tracesField = Traces .class .getDeclaredField ("traces" );
2154+ tracesField .setAccessible (true );
2155+ tracesField .set (mockTraces , traces );
2156+
2157+ var countField = Traces .class .getDeclaredField ("count" );
2158+ countField .setAccessible (true );
2159+ countField .set (mockTraces , 2 );
2160+ } catch (Exception e ) {
2161+ throw new RuntimeException ("Failed to create mock Traces" , e );
2162+ }
2163+
2164+ when (mockContrastSDK .getTraces (eq (TEST_ORG_ID ), eq (TEST_APP_ID ), any (TraceFilterForm .class )))
2165+ .thenReturn (mockTraces );
2166+
2167+ // Mock SDKExtension to return latest session
2168+ var mockAgentSession =
2169+ new com .contrast .labs .ai .mcp .contrast .sdkextension .data .sessionmetadata .AgentSession ();
2170+ mockAgentSession .setAgentSessionId ("latest-session-id" );
2171+ var mockSessionResponse =
2172+ new com .contrast .labs .ai .mcp .contrast .sdkextension .data .sessionmetadata
2173+ .SessionMetadataResponse ();
2174+ mockSessionResponse .setAgentSession (mockAgentSession );
2175+
2176+ try (var mockedSDKExtension =
2177+ mockConstruction (
2178+ com .contrast .labs .ai .mcp .contrast .sdkextension .SDKExtension .class ,
2179+ (mock , context ) -> {
2180+ when (mock .getLatestSessionMetadata (eq (TEST_ORG_ID ), eq (TEST_APP_ID )))
2181+ .thenReturn (mockSessionResponse );
2182+ })) {
2183+
2184+ // When - call with statuses="Confirmed" AND useLatestSession=true
2185+ var result =
2186+ assessService .searchAppVulnerabilities (
2187+ TEST_APP_ID ,
2188+ null , // page
2189+ null , // pageSize
2190+ null , // severities
2191+ "Confirmed" , // statuses (explicit filter)
2192+ null , // vulnTypes
2193+ null , // environments
2194+ null , // lastSeenAfter
2195+ null , // lastSeenBefore
2196+ null , // vulnTags
2197+ null , // sessionMetadataName
2198+ null , // sessionMetadataValue
2199+ true ); // useLatestSession=true
2200+
2201+ // Then - verify SDK received status filters (CRITICAL verification for mcp-3sy)
2202+ var captor = ArgumentCaptor .forClass (TraceFilterForm .class );
2203+ verify (mockContrastSDK ).getTraces (eq (TEST_ORG_ID ), eq (TEST_APP_ID ), captor .capture ());
2204+
2205+ var form = captor .getValue ();
2206+ assertThat (form .getStatus ())
2207+ .as ("Status filters should be passed to SDK even with useLatestSession" )
2208+ .isNotNull ()
2209+ .containsExactly ("Confirmed" );
2210+
2211+ // Verify results: only trace0 matches (Confirmed + latest session)
2212+ // trace1 excluded by in-memory session ID filtering
2213+ assertThat (result .items ())
2214+ .as ("Should return only Confirmed vulnerabilities from latest session" )
2215+ .hasSize (1 );
2216+ assertThat (result .items ().get (0 ).title ()).isEqualTo ("Command Injection" );
2217+ assertThat (result .items ().get (0 ).status ()).isEqualTo ("CONFIRMED" );
2218+ }
2219+ }
2220+
2221+ @ Test
2222+ void searchAppVulnerabilities_should_use_smart_defaults_with_sessionMetadataName ()
2223+ throws Exception {
2224+ // Given - Simulate SDK returning only smart default statuses (SDK filters server-side)
2225+ // Smart defaults = Reported, Suspicious, Confirmed (exclude Fixed and Remediated)
2226+ var mockTraces = new Traces ();
2227+ var traces = new ArrayList <Trace >();
2228+
2229+ // Trace 0: Reported (included in smart defaults), Environment=Production
2230+ Trace trace0 = mock ();
2231+ when (trace0 .getTitle ()).thenReturn ("Reported Vuln" );
2232+ when (trace0 .getRule ()).thenReturn ("rule-0" );
2233+ when (trace0 .getUuid ()).thenReturn ("uuid-0" );
2234+ when (trace0 .getSeverity ()).thenReturn ("HIGH" );
2235+ when (trace0 .getStatus ()).thenReturn ("REPORTED" );
2236+ when (trace0 .getLastTimeSeen ()).thenReturn (System .currentTimeMillis ());
2237+ when (trace0 .getFirstTimeSeen ()).thenReturn (System .currentTimeMillis () - 86400000L );
2238+
2239+ var sessionMetadata0 = mock (SessionMetadata .class );
2240+ var metadataItem0 = mock (MetadataItem .class );
2241+ lenient ().when (metadataItem0 .getDisplayLabel ()).thenReturn ("Environment" );
2242+ lenient ().when (metadataItem0 .getValue ()).thenReturn ("Production" );
2243+ lenient ().when (sessionMetadata0 .getMetadata ()).thenReturn (List .of (metadataItem0 ));
2244+ lenient ().when (trace0 .getSessionMetadata ()).thenReturn (List .of (sessionMetadata0 ));
2245+ traces .add (trace0 );
2246+
2247+ // Trace 1: Confirmed (included in smart defaults), Environment=Production
2248+ Trace trace1 = mock ();
2249+ when (trace1 .getTitle ()).thenReturn ("Confirmed Vuln" );
2250+ when (trace1 .getRule ()).thenReturn ("rule-1" );
2251+ when (trace1 .getUuid ()).thenReturn ("uuid-1" );
2252+ when (trace1 .getSeverity ()).thenReturn ("CRITICAL" );
2253+ when (trace1 .getStatus ()).thenReturn ("CONFIRMED" );
2254+ when (trace1 .getLastTimeSeen ()).thenReturn (System .currentTimeMillis ());
2255+ when (trace1 .getFirstTimeSeen ()).thenReturn (System .currentTimeMillis () - 86400000L );
2256+
2257+ var sessionMetadata1 = mock (SessionMetadata .class );
2258+ var metadataItem1 = mock (MetadataItem .class );
2259+ lenient ().when (metadataItem1 .getDisplayLabel ()).thenReturn ("Environment" );
2260+ lenient ().when (metadataItem1 .getValue ()).thenReturn ("Production" );
2261+ lenient ().when (sessionMetadata1 .getMetadata ()).thenReturn (List .of (metadataItem1 ));
2262+ lenient ().when (trace1 .getSessionMetadata ()).thenReturn (List .of (sessionMetadata1 ));
2263+ traces .add (trace1 );
2264+
2265+ // Trace 2: Reported (included in smart defaults), Environment=QA (excluded by metadata)
2266+ Trace trace2 = mock ();
2267+ when (trace2 .getTitle ()).thenReturn ("QA Vuln" );
2268+ when (trace2 .getRule ()).thenReturn ("rule-2" );
2269+ when (trace2 .getUuid ()).thenReturn ("uuid-2" );
2270+ when (trace2 .getSeverity ()).thenReturn ("MEDIUM" );
2271+ when (trace2 .getStatus ()).thenReturn ("REPORTED" );
2272+ when (trace2 .getLastTimeSeen ()).thenReturn (System .currentTimeMillis ());
2273+ when (trace2 .getFirstTimeSeen ()).thenReturn (System .currentTimeMillis () - 86400000L );
2274+
2275+ var sessionMetadata2 = mock (SessionMetadata .class );
2276+ var metadataItem2 = mock (MetadataItem .class );
2277+ lenient ().when (metadataItem2 .getDisplayLabel ()).thenReturn ("Environment" );
2278+ lenient ().when (metadataItem2 .getValue ()).thenReturn ("QA" );
2279+ lenient ().when (sessionMetadata2 .getMetadata ()).thenReturn (List .of (metadataItem2 ));
2280+ lenient ().when (trace2 .getSessionMetadata ()).thenReturn (List .of (sessionMetadata2 ));
2281+ traces .add (trace2 );
2282+
2283+ // Setup mock traces using reflection
2284+ // Note: Fixed/Remediated traces filtered out by SDK (smart defaults)
2285+ try {
2286+ var tracesField = Traces .class .getDeclaredField ("traces" );
2287+ tracesField .setAccessible (true );
2288+ tracesField .set (mockTraces , traces );
2289+
2290+ var countField = Traces .class .getDeclaredField ("count" );
2291+ countField .setAccessible (true );
2292+ countField .set (mockTraces , 3 );
2293+ } catch (Exception e ) {
2294+ throw new RuntimeException ("Failed to create mock Traces" , e );
2295+ }
2296+
2297+ when (mockContrastSDK .getTraces (eq (TEST_ORG_ID ), eq (TEST_APP_ID ), any (TraceFilterForm .class )))
2298+ .thenReturn (mockTraces );
2299+
2300+ // When - call with sessionMetadataName but NO explicit statuses (triggers smart defaults)
2301+ var result =
2302+ assessService .searchAppVulnerabilities (
2303+ TEST_APP_ID ,
2304+ null , // page
2305+ null , // pageSize
2306+ null , // severities
2307+ null , // statuses (NOT provided - should use smart defaults)
2308+ null , // vulnTypes
2309+ null , // environments
2310+ null , // lastSeenAfter
2311+ null , // lastSeenBefore
2312+ null , // vulnTags
2313+ "Environment" , // sessionMetadataName
2314+ "Production" , // sessionMetadataValue
2315+ null ); // useLatestSession
2316+
2317+ // Then - verify SDK received smart default statuses (CRITICAL verification for mcp-3sy)
2318+ var captor = ArgumentCaptor .forClass (TraceFilterForm .class );
2319+ verify (mockContrastSDK ).getTraces (eq (TEST_ORG_ID ), eq (TEST_APP_ID ), captor .capture ());
2320+
2321+ var form = captor .getValue ();
2322+ assertThat (form .getStatus ())
2323+ .as (
2324+ "Smart default statuses should be passed to SDK (Reported, Suspicious, Confirmed -"
2325+ + " excluding Fixed and Remediated)" )
2326+ .isNotNull ()
2327+ .containsExactlyInAnyOrder ("Reported" , "Suspicious" , "Confirmed" )
2328+ .doesNotContain ("Fixed" , "Remediated" );
2329+
2330+ // Verify results: trace0 and trace1 match (Reported + Confirmed with Environment=Production)
2331+ // trace2 excluded by session metadata filter (Environment=QA)
2332+ // Note: Smart default warning message is tested separately in VulnerabilityFilterParamsTest
2333+ assertThat (result .items ())
2334+ .as ("Should return only actionable statuses (excluding Fixed and Remediated)" )
2335+ .hasSize (2 );
2336+ assertThat (result .items ().get (0 ).title ()).isEqualTo ("Reported Vuln" );
2337+ assertThat (result .items ().get (1 ).title ()).isEqualTo ("Confirmed Vuln" );
2338+ }
19932339}
0 commit comments