Skip to content

Commit

Permalink
Merge pull request #3843 from mercedes-benz/feature-3610-validate-tem…
Browse files Browse the repository at this point in the history
…plate-settings-on-job-creation

Validate templates on job creation + use sealed objects in domain communication
  • Loading branch information
de-jcup authored Feb 13, 2025
2 parents 6874e8e + 6cc1185 commit c0d1904
Show file tree
Hide file tree
Showing 31 changed files with 1,625 additions and 47 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.mercedesbenz.sechub.commons.core;

import static java.util.Objects.*;

import java.time.LocalDateTime;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeMap;
import java.util.TreeSet;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* This provider gives a cached way to provide patterns. Because {@link Pattern}
* is very expensive to create.
*
*/
public class CachingPatternProvider {

private static final Logger LOG = LoggerFactory.getLogger(CachingPatternProvider.class);

private Map<String, PatternCacheEntry> map = new TreeMap<>();
private SortedSet<PatternCacheEntry> sortedEntries = new TreeSet<>();

private int maximumSize;
private PatternCompiler compiler;

private static final Pattern ACCEPT_EVERYTHING_PATTERN = Pattern.compile(".*");

public CachingPatternProvider(int maximumCacheSize, PatternCompiler compiler) {
if (maximumCacheSize < 2) {
throw new IllegalArgumentException("Maximum cache size must be greater than 1!");
}
this.maximumSize = maximumCacheSize;
this.compiler = requireNonNull(compiler);
}

/**
* Resolve compiled pattern for given pattern string.
*
* @param regularExpression regular expression as string
* @return compiled pattern or <code>null</code> if it was not possible to
* create a compiled pattern (because of syntax problems)
*/
public Pattern get(String regularExpression) {
requireNonNull(regularExpression, "regular expression text may not be null!");

PatternCacheEntry entry = map.get(regularExpression);
if (entry == null) {
entry = createNewEntryAndShrinkCacheIfNecesary(regularExpression);

}
return entry.compiledPattern;
}

private PatternCacheEntry createNewEntryAndShrinkCacheIfNecesary(String regularExpression) {
PatternCacheEntry entry = new PatternCacheEntry();
entry.pattern = regularExpression;
try {
Pattern compiledPattern = compiler.compile(regularExpression);
entry.compiledPattern = compiledPattern;

} catch (PatternSyntaxException e) {
/*
* this is a configuration problem at administrator side! We log the problem as
* an error and use an alternative which accepts everything.
*/
entry.compiledPattern = ACCEPT_EVERYTHING_PATTERN;
LOG.error("The pattern is not valid: '{}'. Use instead compiled pattern: '{}'", regularExpression, entry.compiledPattern, e);
}

map.put(regularExpression, entry);
sortedEntries.add(entry); // we add it here for cleanup

/* check max size */
while (sortedEntries.size() > maximumSize) {
PatternCacheEntry removeMe = sortedEntries.iterator().next();

/* remove from both collections: */
sortedEntries.remove(removeMe);
map.remove(removeMe.pattern);
}
return entry;
}

private class PatternCacheEntry implements Comparable<PatternCacheEntry> {
private Pattern compiledPattern;
private String pattern;
private LocalDateTime created;

private PatternCacheEntry() {
this.created = LocalDateTime.now();
}

@Override
public int compareTo(PatternCacheEntry o) {
if (o == null) {
return 1;
}
return created.compareTo(o.created);
}

@Override
public String toString() {
return "PatternCacheEntry [pattern=" + pattern + ",created=" + created + ", compiledPattern=" + compiledPattern + "]";
}

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.mercedesbenz.sechub.commons.core;

import static java.util.Objects.*;

import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

public class PatternCompiler {

/**
* Compiles regular expression to a pattern
*
* @param regExp regular expression
* @return pattern, never {@link NullPointerException}
* @throws PatternSyntaxException
*/
public Pattern compile(String regExp) {
return Pattern.compile(requireNonNull(regExp, "Regular expression may not be null!"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
package com.mercedesbenz.sechub.commons.core;

import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;

import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.junit.jupiter.api.Test;

class CachingPatternProviderTest {

@Test
void cache_maximum_2_wrong_regular_expression_results_in_alternative_accepting_everything() {

/* prepare */
PatternCompiler compiler = mock();
when(compiler.compile(any())).thenThrow(new PatternSyntaxException("desc", "regex", 0));
CachingPatternProvider providerToTest = new CachingPatternProvider(2, compiler);

/* execute */
Pattern result = providerToTest.get("i-am-simulated-wrong");

/* test */
assertThat(result.toString()).isEqualTo(".*");
}

@Test
void cache_maximum_2_correct_regular_expression_results_in_correct_compiled_pattern() {

/* prepare */
PatternCompiler compiler = mock();
Pattern pattern = mock();
when(compiler.compile(eq("i-am-correct"))).thenReturn(pattern);
CachingPatternProvider providerToTest = new CachingPatternProvider(2, compiler);

/* execute */
Pattern result = providerToTest.get("i-am-correct");

/* test */
verify(compiler).compile("i-am-correct");
assertThat(result).isSameAs(pattern);
}

@Test
void cache_maximum_2_correct_regular_expression_fetched_3_times_created_only_one_time() {

/* prepare */
PatternCompiler compiler = mock();
Pattern pattern = mock();
when(compiler.compile(eq("i-am-correct"))).thenReturn(pattern);
CachingPatternProvider providerToTest = new CachingPatternProvider(2, compiler);

/* execute */
Pattern result = providerToTest.get("i-am-correct");
result = providerToTest.get("i-am-correct");
result = providerToTest.get("i-am-correct");

/* test */
verify(compiler, times(1)).compile("i-am-correct");
assertThat(result).isSameAs(pattern);
}

@Test
void cache_maximum_2_handling_3_regular_expressions_does_remove_older_entries_automatically_when_cache_size_limit_reached() {

/* prepare */
PatternCompiler compiler = mock();
Pattern pattern1 = mock();
Pattern pattern2 = mock();
Pattern pattern3 = mock();

when(compiler.compile(eq("p1"))).thenReturn(pattern1);
when(compiler.compile(eq("p2"))).thenReturn(pattern2);
when(compiler.compile(eq("p3"))).thenReturn(pattern3);
CachingPatternProvider providerToTest = new CachingPatternProvider(2, compiler);

/* execute 1 - empty cache */
Pattern result1 = providerToTest.get("p1"); // p1 added (one left)
Pattern result2 = providerToTest.get("p2"); // p2 added (zero left)

/* test 1 - empty cache, both patterns are compiled */
var expectedCompileP1amount = 1;
var expectedCompileP2amount = 1;

verify(compiler, times(expectedCompileP1amount)).compile("p1");
verify(compiler, times(expectedCompileP2amount)).compile("p2");

/* execute 2 - use cache only */
for (int i = 0; i < 10; i++) {
/* execute 0 */
result1 = providerToTest.get("p1"); // must be from cache
result2 = providerToTest.get("p2"); // must be from cache
}

/* test 2 - test fetching existing ones does not compile again */
verify(compiler, times(expectedCompileP1amount)).compile("p1");
verify(compiler, times(expectedCompileP2amount)).compile("p2");

/* execute 3 - recreate removed pattern */
result1 = providerToTest.get("p1"); // p1 added (one left)
result2 = providerToTest.get("p2"); // p2 added (zero left)
Pattern result3 = providerToTest.get("p3"); // p3 added, must remove p1 fetch here because oldest
Pattern result1x = providerToTest.get("p1"); // fetch p1 again... will recreated

/* test 3 - data is still correct and re-creation works */
expectedCompileP1amount++;// p1 must be recreated (p1,p2,p3->p1 removed, p1 fetch--> recreate)
var expectedCompileP3amount = 1;

verify(compiler, times(expectedCompileP2amount)).compile("p2");
verify(compiler, times(expectedCompileP3amount)).compile("p3");
verify(compiler, times(expectedCompileP1amount)).compile("p1");

assertThat(result1).isSameAs(pattern1);
assertThat(result1x).isSameAs(pattern1);
assertThat(result2).isSameAs(pattern2);
assertThat(result3).isSameAs(pattern3);

/* execute 4 - use cached values */
result1 = providerToTest.get("p1");
result3 = providerToTest.get("p3");

/* test 4 */
verify(compiler, times(expectedCompileP1amount)).compile("p1"); // p1 must not be created again
verify(compiler, times(expectedCompileP3amount)).compile("p3"); // p3 must not be created again

assertThat(result1).isSameAs(pattern1);
assertThat(result3).isSameAs(pattern3);

/* execute 5 - use next entry, not in cache, and max reached */
result2 = providerToTest.get("p2");

/* test 5 */
expectedCompileP2amount++; // p2 must be recreated
verify(compiler, times(expectedCompileP2amount)).compile("p2");
assertThat(result2).isSameAs(pattern2);

/* execute 6 - next entry not */
result2 = providerToTest.get("p2");

/* test 6 */
verify(compiler, times(expectedCompileP2amount)).compile("p2"); // same expectedCompileP2amount as before - means comes from cache
assertThat(result2).isSameAs(pattern2);

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.mercedesbenz.sechub.commons.core;

import static org.assertj.core.api.Assertions.*;

import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;

class PatternCompilerTest {

private PatternCompiler compilerToTest;

@BeforeEach
void beforeEach() {
compilerToTest = new PatternCompiler();
}

@Test
void wrong_regular_expresion_throws_pattern_syntax_exception() {
assertThatThrownBy(() -> compilerToTest.compile("i-am-wrong(")).isInstanceOf(PatternSyntaxException.class);
}

@Test
void null_regular_expresion_throws_npe() {
assertThatThrownBy(() -> compilerToTest.compile(null)).isInstanceOf(NullPointerException.class).hasMessageContaining("may not be null");
}

@ParameterizedTest
@ValueSource(strings = { "abc", ".*" })
void correct_regular_expresion_results_in_pattern(String regExp) {
/* execute */
Pattern result = compilerToTest.compile(regExp);

/* test */
assertThat(result).isNotNull();
assertThat(result.toString()).isEqualTo(regExp);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -173,12 +173,12 @@ private boolean atLeastOneNameReferencesOneElementInGivenDataConfiguration(Set<S
}

/**
* Collects scan types
* Collects scan types used inside given configuration model
*
* @param model
* @param model configuration model to inspect
* @return set with scan types, never <code>null</code>
*/
public Set<ScanType> collectPublicScanTypes(SecHubConfigurationModel model) {
public Set<ScanType> collectScanTypes(SecHubConfigurationModel model) {
Set<ScanType> result = new LinkedHashSet<>();
if (model.getCodeScan().isPresent()) {
result.add(ScanType.CODE_SCAN);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ private void handleMetaData(InternalValidationContext context) {
}

private void handleScanTypesAndModuleGroups(InternalValidationContext context) {
Set<ScanType> scanTypes = modelSupport.collectPublicScanTypes(context.model);
Set<ScanType> scanTypes = modelSupport.collectScanTypes(context.model);
handleScanTypes(context, scanTypes);

handleModuleGroup(context, scanTypes);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
package com.mercedesbenz.sechub.commons.model.template;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;

/**
* Template data for SecHub configuration model. Here users can define user
Expand All @@ -13,9 +13,15 @@
*/
public class TemplateData {

private Map<String, String> variables = new LinkedHashMap<>();
private SortedMap<String, String> variables = new TreeMap<>();

public Map<String, String> getVariables() {
/**
* Return a sorted map containing variable names as keys and variable values as
* values.
*
* @return sorted variable map
*/
public SortedMap<String, String> getVariables() {
return variables;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,13 @@

public class TemplateDataResolver {

/**
* Resolves template data for given type
*
* @param type template type
* @param configuration SecHub configuration
* @return data or <code>null</code>
*/
public TemplateData resolveTemplateData(TemplateType type, SecHubConfigurationModel configuration) {
if (type == null) {
return null;
Expand Down
Loading

0 comments on commit c0d1904

Please sign in to comment.