Skip to content

Commit 6d39fcc

Browse files
authored
Merge pull request #153 from snyk/feat/allowlist-last-modified-date
feat: Add allowlist for last modified date
2 parents 7f7848e + 0c14d13 commit 6d39fcc

6 files changed

Lines changed: 141 additions & 13 deletions

File tree

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,16 @@ snyk.api.organization=
7272
# Default: 0
7373
#snyk.scanner.lastModified.days=0
7474

75-
# If remoteOnly is set to true, only check lastModified for packages contained in remote repositories.
76-
# Default: true
77-
#snyk.scanner.lastModified.remoteOnly=true
75+
# Comma-separated substrings matched against the repository key. If the key contains any of these substrings,
76+
# the last-modified delay check is not applied for that repository. When empty, last-modified applies to all
77+
# repositories (unless legacy remoteOnly below is used). Example: "-local,cache,virtual"
78+
# Default: (empty)
79+
#snyk.scanner.lastModified.allowlist=
80+
81+
# Deprecated: when snyk.scanner.lastModified.allowlist is empty and this is true, only remote repositories
82+
# receive the last-modified delay check. Ignored when allowlist is non-empty. Prefer allowlist.
83+
# Default: false
84+
#snyk.scanner.lastModified.remoteOnly=false
7885

7986
# By default, if Snyk API fails while scanning an artifact for any reason, the download will be allowed.
8087
# When a download is blocked, artifact property "snyk.block.reason" records the message (see README).

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ 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_ALLOWLIST("snyk.scanner.lastModified.allowlist", ""),
38+
/**
39+
* @deprecated Use {@link #SCANNER_LAST_MODIFIED_ALLOWLIST} to control which repository keys skip the last-modified delay check.
40+
*/
41+
@Deprecated
42+
SCANNER_LAST_MODIFIED_CHECK_ONLY_REMOTE("snyk.scanner.lastModified.remoteOnly", "false");
3843

3944
private final String propertyKey;
4045
private final String defaultValue;
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package io.snyk.plugins.artifactory.scanner;
2+
3+
import java.util.ArrayList;
4+
import java.util.Collections;
5+
import java.util.List;
6+
7+
/**
8+
* Parses {@link io.snyk.plugins.artifactory.configuration.PluginConfiguration#SCANNER_LAST_MODIFIED_ALLOWLIST}
9+
* and decides whether a repository key matches any allowlisted substring.
10+
*/
11+
final class LastModifiedRepositoryPolicy {
12+
13+
private LastModifiedRepositoryPolicy() {}
14+
15+
static List<String> parseAllowlist(String raw) {
16+
if (raw == null || raw.isBlank()) {
17+
return List.of();
18+
}
19+
List<String> out = new ArrayList<>();
20+
for (String segment : raw.split(",")) {
21+
String t = segment.trim();
22+
if (!t.isEmpty()) {
23+
out.add(t);
24+
}
25+
}
26+
return out.isEmpty() ? List.of() : Collections.unmodifiableList(out);
27+
}
28+
29+
static boolean repoKeyMatchesAllowlist(String repoKey, List<String> substrings) {
30+
for (String s : substrings) {
31+
if (repoKey.contains(s)) {
32+
return true;
33+
}
34+
}
35+
return false;
36+
}
37+
}

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

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@
2424
import javax.annotation.Nonnull;
2525
import java.time.Duration;
2626
import java.time.Instant;
27+
import java.util.List;
2728
import java.util.Optional;
29+
import java.util.concurrent.atomic.AtomicBoolean;
2830

2931
import static io.snyk.plugins.artifactory.configuration.properties.ArtifactProperty.BLOCK_REASON;
3032
import static java.util.Objects.requireNonNull;
3133

3234
public class ScannerModule {
3335
private static final Logger LOG = LoggerFactory.getLogger(ScannerModule.class);
36+
private static final AtomicBoolean LOGGED_DEPRECATED_REMOTE_ONLY = new AtomicBoolean(false);
37+
private static final AtomicBoolean LOGGED_REMOTE_ONLY_IGNORED_WITH_ALLOWLIST = new AtomicBoolean(false);
3438
private final ConfigurationModule configurationModule;
3539
private final Repositories repositories;
3640
private final EcosystemResolver ecosystemResolver;
@@ -125,15 +129,10 @@ private void filter(RepoPath repoPath, MonitoredArtifact artifact) {
125129
}
126130

127131
private Instant getLastModifiedDate(RepoPath repoPath) {
128-
// Only apply lastModifiedDate to packages from remote repositories.
129-
if(lastModifiedDateRemoteOnly()) {
130-
LOG.debug("Last modified date applied to only remote repositories.");
131-
if (!isRemoteRepository(repoPath)) {
132-
LOG.debug("Provided repository is not a remote repository, skipping last modified date check for {}", repoPath);
133-
return null;
134-
}
132+
if (shouldSkipLastModifiedForRepository(repoPath)) {
133+
return null;
135134
}
136-
135+
137136
try {
138137
ItemInfo itemInfo = repositories.getItemInfo(repoPath);
139138
if (itemInfo != null) {
@@ -149,6 +148,45 @@ private Instant getLastModifiedDate(RepoPath repoPath) {
149148
return null;
150149
}
151150

151+
/**
152+
* When {@link PluginConfiguration#SCANNER_LAST_MODIFIED_ALLOWLIST} is non-empty, repository keys containing any
153+
* configured substring skip the last-modified delay check. When the allowlist is empty and legacy
154+
* {@link PluginConfiguration#SCANNER_LAST_MODIFIED_CHECK_ONLY_REMOTE} is true, only remote repositories keep the check.
155+
*/
156+
private boolean shouldSkipLastModifiedForRepository(RepoPath repoPath) {
157+
String allowlistRaw = configurationModule.getPropertyOrDefault(PluginConfiguration.SCANNER_LAST_MODIFIED_ALLOWLIST);
158+
List<String> allowlist = LastModifiedRepositoryPolicy.parseAllowlist(allowlistRaw);
159+
String repoKey = repoPath.getRepoKey();
160+
161+
if (!allowlist.isEmpty()) {
162+
if (lastModifiedDateRemoteOnly() && LOGGED_REMOTE_ONLY_IGNORED_WITH_ALLOWLIST.compareAndSet(false, true)) {
163+
LOG.warn(
164+
"snyk.scanner.lastModified.remoteOnly is set but ignored because snyk.scanner.lastModified.allowlist is configured; remove remoteOnly."
165+
);
166+
}
167+
if (LastModifiedRepositoryPolicy.repoKeyMatchesAllowlist(repoKey, allowlist)) {
168+
LOG.debug("Repository key matches last-modified allowlist, skipping last modified date for {}", repoPath);
169+
return true;
170+
}
171+
return false;
172+
}
173+
174+
if (lastModifiedDateRemoteOnly()) {
175+
if (LOGGED_DEPRECATED_REMOTE_ONLY.compareAndSet(false, true)) {
176+
LOG.warn(
177+
"snyk.scanner.lastModified.remoteOnly is deprecated; use snyk.scanner.lastModified.allowlist to skip the last-modified check for specific repository keys."
178+
);
179+
}
180+
if (!isRemoteRepository(repoPath)) {
181+
LOG.debug("Legacy remoteOnly: repository is not remote, skipping last modified date for {}", repoPath);
182+
return true;
183+
}
184+
}
185+
186+
return false;
187+
}
188+
189+
/** Used only when {@link PluginConfiguration#SCANNER_LAST_MODIFIED_CHECK_ONLY_REMOTE} is true and the allowlist is empty. */
152190
private boolean isRemoteRepository(RepoPath repoPath) {
153191
String repoKey = repoPath.getRepoKey();
154192
RepositoryConfiguration repoConfig = repositories.getRepositoryConfiguration(repoKey);
@@ -163,6 +201,7 @@ private boolean shouldTestContinuously() {
163201
return configurationModule.getPropertyOrDefault(PluginConfiguration.TEST_CONTINUOUSLY).equals("true");
164202
}
165203

204+
@SuppressWarnings("deprecation")
166205
private boolean lastModifiedDateRemoteOnly() {
167206
return configurationModule.getPropertyOrDefault(PluginConfiguration.SCANNER_LAST_MODIFIED_CHECK_ONLY_REMOTE).equals("true");
168207
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package io.snyk.plugins.artifactory.scanner;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.List;
6+
7+
import static org.junit.jupiter.api.Assertions.assertEquals;
8+
import static org.junit.jupiter.api.Assertions.assertFalse;
9+
import static org.junit.jupiter.api.Assertions.assertTrue;
10+
11+
class LastModifiedRepositoryPolicyTest {
12+
13+
@Test
14+
void parseAllowlist_nullOrBlank_returnsEmpty() {
15+
assertTrue(LastModifiedRepositoryPolicy.parseAllowlist(null).isEmpty());
16+
assertTrue(LastModifiedRepositoryPolicy.parseAllowlist("").isEmpty());
17+
assertTrue(LastModifiedRepositoryPolicy.parseAllowlist(" ").isEmpty());
18+
assertTrue(LastModifiedRepositoryPolicy.parseAllowlist(" , , ").isEmpty());
19+
}
20+
21+
@Test
22+
void parseAllowlist_splitsTrimsAndDropsEmptySegments() {
23+
assertEquals(List.of("a", "b"), LastModifiedRepositoryPolicy.parseAllowlist("a,b"));
24+
assertEquals(List.of("x", "y"), LastModifiedRepositoryPolicy.parseAllowlist(" x , y "));
25+
assertEquals(List.of("one"), LastModifiedRepositoryPolicy.parseAllowlist("one,,,"));
26+
}
27+
28+
@Test
29+
void repoKeyMatchesAllowlist_substringMatch() {
30+
List<String> patterns = List.of("-local", "cache");
31+
assertTrue(LastModifiedRepositoryPolicy.repoKeyMatchesAllowlist("npm-local", patterns));
32+
assertTrue(LastModifiedRepositoryPolicy.repoKeyMatchesAllowlist("maven-cache-remote", patterns));
33+
assertFalse(LastModifiedRepositoryPolicy.repoKeyMatchesAllowlist("npm-remote", patterns));
34+
}
35+
36+
@Test
37+
void repoKeyMatchesAllowlist_emptyPatterns_neverMatches() {
38+
assertFalse(LastModifiedRepositoryPolicy.repoKeyMatchesAllowlist("anything", List.of()));
39+
}
40+
}

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
<version.maven.resources.plugin>3.1.0</version.maven.resources.plugin>
3939
<version.maven.site.plugin>3.7.1</version.maven.site.plugin>
4040
<version.maven.surefire.plugin>3.0.0-M3</version.maven.surefire.plugin>
41-
<jackson-databind.version>2.21.1</jackson-databind.version>
41+
<jackson-databind.version>2.21.2</jackson-databind.version>
4242
</properties>
4343

4444
<build>

0 commit comments

Comments
 (0)