Skip to content

Commit b3f8968

Browse files
committed
feat: Add feature to differentiate between local and remote repositories using repo name string matching
1 parent 7f7848e commit b3f8968

5 files changed

Lines changed: 165 additions & 4 deletions

File tree

core/src/main/groovy/io/snyk/plugins/artifactory/snykSecurityPlugin.properties

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,16 @@ snyk.api.organization=
7676
# Default: true
7777
#snyk.scanner.lastModified.remoteOnly=true
7878

79+
# Optional: treat repositories whose key contains any of the given comma-separated substrings as local (not remote)
80+
# for lastModified.remoteOnly behavior, regardless of Artifactory's repository type.
81+
# Matching is case-insensitive. When enabled but no pattern matches the repo key, classification falls back to
82+
# Artifactory's repository type (remote vs local).
83+
# Accepts: "true", "false"
84+
# Default: "false"
85+
#snyk.scanner.repository.local.nameMatch.enabled=false
86+
# Comma-separated substrings; empty entries are ignored. Example: libs-release,internal-local
87+
#snyk.scanner.repository.local.nameMatch.patterns=
88+
7989
# By default, if Snyk API fails while scanning an artifact for any reason, the download will be allowed.
8090
# When a download is blocked, artifact property "snyk.block.reason" records the message (see README).
8191
# Setting this property to "true" will block downloads when Snyk API fails.

core/src/main/java/io/snyk/plugins/artifactory/configuration/PluginConfiguration.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ public enum PluginConfiguration implements Configuration {
3434
TEST_FREQUENCY_HOURS("snyk.scanner.frequency.hours", "168"),
3535
EXTEND_TEST_DEADLINE_HOURS("snyk.scanner.extendTestDeadline.hours", "24"),
3636
SCANNER_LAST_MODIFIED_DELAY_DAYS("snyk.scanner.lastModified.days", "0"),
37-
SCANNER_LAST_MODIFIED_CHECK_ONLY_REMOTE("snyk.scanner.lastModified.remoteOnly", "true");
37+
SCANNER_LAST_MODIFIED_CHECK_ONLY_REMOTE("snyk.scanner.lastModified.remoteOnly", "true"),
38+
SCANNER_REPOSITORY_LOCAL_NAME_MATCH_ENABLED("snyk.scanner.repository.local.nameMatch.enabled", "false"),
39+
SCANNER_REPOSITORY_LOCAL_NAME_MATCH_PATTERNS("snyk.scanner.repository.local.nameMatch.patterns", "");
3840

3941
private final String propertyKey;
4042
private final String defaultValue;
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package io.snyk.plugins.artifactory.scanner;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
import java.util.Locale;
7+
8+
/**
9+
* Classifies whether a repository is treated as remote for last-modified checks, combining optional
10+
* substring matching on the repository key with Artifactory's configured repository type.
11+
*/
12+
public final class RepositoryRemoteClassifier {
13+
14+
private RepositoryRemoteClassifier() {}
15+
16+
static List<String> parseCommaSeparatedPatterns(String raw) {
17+
if (raw == null || raw.isBlank()) {
18+
return List.of();
19+
}
20+
List<String> lowered = new ArrayList<>();
21+
for (String part : raw.split(",")) {
22+
String trimmed = part.trim();
23+
if (!trimmed.isEmpty()) {
24+
lowered.add(trimmed.toLowerCase(Locale.ROOT));
25+
}
26+
}
27+
return Collections.unmodifiableList(lowered);
28+
}
29+
30+
/**
31+
* @return true if {@code repoKey} contains any of the patterns (case-insensitive substring match).
32+
*/
33+
static boolean repoKeyContainsAnyPattern(String repoKey, List<String> patternsLowercase) {
34+
if (repoKey == null || patternsLowercase.isEmpty()) {
35+
return false;
36+
}
37+
String keyLower = repoKey.toLowerCase(Locale.ROOT);
38+
for (String pattern : patternsLowercase) {
39+
if (keyLower.contains(pattern)) {
40+
return true;
41+
}
42+
}
43+
return false;
44+
}
45+
46+
/**
47+
* When local name matching is enabled and a pattern matches, the repository is not treated as remote.
48+
* Otherwise {@code artifactoryTypeIsRemote} is used.
49+
*/
50+
static boolean isRemoteRepository(
51+
String repoKey,
52+
boolean localNameMatchEnabled,
53+
String patternsRaw,
54+
boolean artifactoryTypeIsRemote
55+
) {
56+
if (localNameMatchEnabled) {
57+
List<String> patterns = parseCommaSeparatedPatterns(patternsRaw);
58+
if (!patterns.isEmpty() && repoKeyContainsAnyPattern(repoKey, patterns)) {
59+
return false;
60+
}
61+
}
62+
return artifactoryTypeIsRemote;
63+
}
64+
}

core/src/main/java/io/snyk/plugins/artifactory/scanner/ScannerModule.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,12 +151,25 @@ private Instant getLastModifiedDate(RepoPath repoPath) {
151151

152152
private boolean isRemoteRepository(RepoPath repoPath) {
153153
String repoKey = repoPath.getRepoKey();
154+
String patternsRaw = configurationModule.getPropertyOrDefault(PluginConfiguration.SCANNER_REPOSITORY_LOCAL_NAME_MATCH_PATTERNS);
155+
boolean localNameMatchEnabled = localNameMatchEnabled();
156+
154157
RepositoryConfiguration repoConfig = repositories.getRepositoryConfiguration(repoKey);
155-
String repoType = repoConfig.getType();
158+
boolean artifactoryTypeIsRemote;
159+
if (repoConfig == null) {
160+
LOG.debug("No repository configuration for {}, cannot read repository type", repoKey);
161+
artifactoryTypeIsRemote = false;
162+
} else {
163+
String repoType = repoConfig.getType();
164+
LOG.debug("Found repository type: {}", repoType);
165+
artifactoryTypeIsRemote = repoType != null && repoType.equalsIgnoreCase("remote");
166+
}
156167

157-
LOG.debug("Found repository type: {}", repoType);
168+
return RepositoryRemoteClassifier.isRemoteRepository(repoKey, localNameMatchEnabled, patternsRaw, artifactoryTypeIsRemote);
169+
}
158170

159-
return repoType.toLowerCase().equals("remote");
171+
private boolean localNameMatchEnabled() {
172+
return configurationModule.getPropertyOrDefault(PluginConfiguration.SCANNER_REPOSITORY_LOCAL_NAME_MATCH_ENABLED).equals("true");
160173
}
161174

162175
private boolean shouldTestContinuously() {
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package io.snyk.plugins.artifactory.scanner;
2+
3+
import org.junit.jupiter.api.DisplayName;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.util.List;
7+
8+
import static org.junit.jupiter.api.Assertions.assertAll;
9+
import static org.junit.jupiter.api.Assertions.assertEquals;
10+
import static org.junit.jupiter.api.Assertions.assertFalse;
11+
import static org.junit.jupiter.api.Assertions.assertTrue;
12+
13+
@DisplayName("RepositoryRemoteClassifier")
14+
class RepositoryRemoteClassifierTest {
15+
16+
@DisplayName("parseCommaSeparatedPatterns trims, ignores empties, lowercases")
17+
@Test
18+
void parseCommaSeparatedPatterns() {
19+
assertAll(
20+
() -> assertTrue(RepositoryRemoteClassifier.parseCommaSeparatedPatterns("").isEmpty()),
21+
() -> assertTrue(RepositoryRemoteClassifier.parseCommaSeparatedPatterns(" , ").isEmpty()),
22+
() -> assertEquals(
23+
List.of("a", "bc"),
24+
RepositoryRemoteClassifier.parseCommaSeparatedPatterns(" a , bc ")
25+
),
26+
() -> assertEquals(
27+
List.of("libs-release", "internal"),
28+
RepositoryRemoteClassifier.parseCommaSeparatedPatterns("Libs-Release, INTERNAL")
29+
)
30+
);
31+
}
32+
33+
@DisplayName("repoKeyContainsAnyPattern is case-insensitive substring match")
34+
@Test
35+
void repoKeyContainsAnyPattern() {
36+
List<String> p = List.of("remote", "cache");
37+
assertAll(
38+
() -> assertFalse(RepositoryRemoteClassifier.repoKeyContainsAnyPattern(null, p)),
39+
() -> assertFalse(RepositoryRemoteClassifier.repoKeyContainsAnyPattern("my-local", List.of())),
40+
() -> assertTrue(RepositoryRemoteClassifier.repoKeyContainsAnyPattern("MY-REMOTE-REPO", p)),
41+
() -> assertTrue(RepositoryRemoteClassifier.repoKeyContainsAnyPattern("pypi-cache", p)),
42+
() -> assertFalse(RepositoryRemoteClassifier.repoKeyContainsAnyPattern("clean", p))
43+
);
44+
}
45+
46+
@DisplayName("isRemoteRepository: feature off uses Artifactory type only")
47+
@Test
48+
void featureOffUsesArtifactoryOnly() {
49+
assertTrue(RepositoryRemoteClassifier.isRemoteRepository("any", false, "local", true));
50+
assertFalse(RepositoryRemoteClassifier.isRemoteRepository("any", false, "local", false));
51+
}
52+
53+
@DisplayName("isRemoteRepository: enabled with empty patterns falls back to Artifactory type")
54+
@Test
55+
void enabledEmptyPatternsFallsBack() {
56+
assertTrue(RepositoryRemoteClassifier.isRemoteRepository("my-local", true, "", true));
57+
assertFalse(RepositoryRemoteClassifier.isRemoteRepository("my-local", true, " , ", false));
58+
}
59+
60+
@DisplayName("isRemoteRepository: pattern match forces non-remote")
61+
@Test
62+
void patternMatchForcesLocal() {
63+
assertFalse(RepositoryRemoteClassifier.isRemoteRepository("company-local-repo", true, "local", true));
64+
}
65+
66+
@DisplayName("isRemoteRepository: no pattern match falls back to Artifactory type")
67+
@Test
68+
void noPatternMatchFallsBack() {
69+
assertTrue(RepositoryRemoteClassifier.isRemoteRepository("remote-npm", true, "local-only", true));
70+
assertFalse(RepositoryRemoteClassifier.isRemoteRepository("remote-npm", true, "local-only", false));
71+
}
72+
}

0 commit comments

Comments
 (0)