Skip to content

Commit a59e52b

Browse files
Add Long-Running Scan Infrastructure for Async External Scanners (#1565)
* fix broken extension icons on scan cards * Fix line endings * Add long running scans. Refactored publish checks --------- Co-authored-by: Alejandro Rivera <alejandro.rivera1996@gmail.com>
1 parent 2f92511 commit a59e52b

File tree

100 files changed

+12914
-2969
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

100 files changed

+12914
-2969
lines changed

server/build.gradle

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,8 @@ def versions = [
4444
gatling: '3.14.9',
4545
loki4j: '1.4.2',
4646
jedis: '6.2.0',
47-
re2j: '1.7'
47+
re2j: '1.7',
48+
jsonpath: '2.9.0'
4849
]
4950
ext['junit-jupiter.version'] = versions.junit
5051
java {
@@ -139,6 +140,7 @@ dependencies {
139140
implementation "org.apache.httpcomponents.client5:httpclient5"
140141
implementation "org.apache.tika:tika-core:${versions.tika}"
141142
implementation "com.google.re2j:re2j:${versions.re2j}"
143+
implementation "com.jayway.jsonpath:json-path:${versions.jsonpath}"
142144
implementation "com.github.loki4j:loki-logback-appender:${versions.loki4j}"
143145
implementation "io.micrometer:micrometer-tracing"
144146
implementation "io.micrometer:micrometer-tracing-bridge-otel"

server/src/dev/resources/application.yml

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -172,28 +172,37 @@ ovsx:
172172
template: 'revoked-access-tokens.html'
173173
scanning:
174174
enabled: true
175-
similarity:
176-
enabled: true
177-
enforced: true
178-
levenshtein-threshold: 0.2
179-
skip-verified-publishers: true
180-
check-against-verified-only: true
181-
exclude-owner-namespaces: true
182-
new-extensions-only: true
183-
secret-scanning:
184-
enabled: true
185-
rules-path: 'classpath:scanning/secret-scanning-custom-rules.yaml'
186-
inline-suppressions: 'secret-scanner:ignore,gitleaks:allow,nosecret,@suppress-secret'
187-
auto-generate-rules: true
188-
force-regenerate-rules: false
189-
generated-rules-path: '/tmp/secret-scanning-rules-gitleaks.yaml'
190-
max-file-size-bytes: 5242880
191-
max-entry-count: 50000
192-
max-total-uncompressed-bytes: 524288000
193-
max-findings: 200
194-
max-line-length: 10000
195-
long-line-no-space-threshold: 1000
196-
keyword-context-chars: 100
197-
log-allowlisted-value-preview-length: 10
198-
timeout-seconds: 5
199-
timeout-check-every-n-lines: 100
175+
# Shared archive limits for all scanning checks (secret detection, blocklist, etc.)
176+
max-archive-size-bytes: 1073741824 # 1 GB total archive limit
177+
max-single-file-bytes: 268435456 # 256 MB per-file limit
178+
max-entry-count: 100000 # Max ZIP entries to process
179+
blocklist-check:
180+
enabled: true
181+
enforced: false
182+
similarity:
183+
enabled: true
184+
enforced: false
185+
similarity-threshold: 0.2
186+
skip-if-publisher-verified: false
187+
only-protect-verified-names: false
188+
allow-similarity-to-own-names: true
189+
only-check-new-extensions: true
190+
secret-detection:
191+
enabled: true
192+
enforced: false
193+
rules-path: 'classpath:scanning/secret-detection-custom-rules.yaml'
194+
suppression-markers: 'secret-detector:ignore,gitleaks:allow,nosecret,@suppress-secret'
195+
gitleaks:
196+
auto-fetch: true
197+
force-refresh: true
198+
output-path: '/tmp/secret-detection-rules-gitleaks.yaml'
199+
scheduled-refresh: true
200+
refresh-cron: '0 0 3 * * *' # Daily at 3 AM
201+
skip-rule-ids: 'generic-api-key' # Rule IDs that produce too many false positives
202+
max-findings: 200
203+
minified-line-threshold: 10000
204+
long-line-no-space-threshold: 1000
205+
regex-context-chars: 100
206+
debug-preview-chars: 10
207+
timeout-seconds: 10
208+
timeout-check-interval: 100

server/src/main/java/org/eclipse/openvsx/ExtensionService.java

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.eclipse.openvsx.json.TargetPlatformVersionJson;
2323
import org.eclipse.openvsx.publish.PublishExtensionVersionHandler;
2424
import org.eclipse.openvsx.repositories.RepositoryService;
25+
import org.eclipse.openvsx.scanning.ExtensionScanPersistenceService;
2526
import org.eclipse.openvsx.scanning.ExtensionScanService;
2627
import org.eclipse.openvsx.search.SearchUtilService;
2728
import org.eclipse.openvsx.util.ErrorResultException;
@@ -56,6 +57,7 @@ public class ExtensionService {
5657
private final PublishExtensionVersionHandler publishHandler;
5758
private final JobRequestScheduler scheduler;
5859
private final ExtensionScanService scanService;
60+
private final ExtensionScanPersistenceService scanPersistenceService;
5961

6062
@Value("${ovsx.publishing.require-license:false}")
6163
boolean requireLicense;
@@ -70,7 +72,8 @@ public ExtensionService(
7072
CacheService cache,
7173
PublishExtensionVersionHandler publishHandler,
7274
JobRequestScheduler scheduler,
73-
ExtensionScanService scanService
75+
ExtensionScanService scanService,
76+
ExtensionScanPersistenceService scanPersistenceService
7477
) {
7578
this.entityManager = entityManager;
7679
this.repositories = repositories;
@@ -79,6 +82,7 @@ public ExtensionService(
7982
this.publishHandler = publishHandler;
8083
this.scheduler = scheduler;
8184
this.scanService = scanService;
85+
this.scanPersistenceService = scanPersistenceService;
8286
}
8387

8488
@Transactional
@@ -111,9 +115,8 @@ private ExtensionVersion publishVersionWithScan(InputStream content, PersonalAcc
111115
scanService.runValidation(scan, extensionFile, token.getUser());
112116

113117
doPublish(extensionFile, null, token, TimeUtil.getCurrentUTC(), true);
114-
115-
scanService.runScan(scan, extensionFile, token.getUser());
116118

119+
// Publish async handles requesting the longrunning scans
117120
publishHandler.publishAsync(extensionFile, this, scan);
118121
var download = extensionFile.getResource();
119122
publishHandler.schedulePublicIdJob(extensionFile.getResource());
@@ -290,6 +293,10 @@ protected ResultJson deleteExtension(ExtensionVersion extVersion) {
290293
}
291294

292295
private void removeExtensionVersion(ExtensionVersion extVersion) {
296+
// Clean up any pending scan jobs for this extension version
297+
// to prevent "file not found" errors after deletion
298+
scanPersistenceService.deleteScansForExtensionVersion(extVersion.getId());
299+
293300
repositories.findFiles(extVersion).map(RemoveFileJobRequest::new).forEach(scheduler::enqueue);
294301
repositories.deleteFiles(extVersion);
295302
entityManager.remove(extVersion);

server/src/main/java/org/eclipse/openvsx/UserAPI.java

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111

1212
import jakarta.servlet.http.HttpServletRequest;
1313
import org.eclipse.openvsx.eclipse.EclipseService;
14+
import org.eclipse.openvsx.entities.ExtensionVersion;
1415
import org.eclipse.openvsx.entities.NamespaceMembership;
16+
import org.eclipse.openvsx.entities.ScanStatus;
1517
import org.eclipse.openvsx.entities.UserData;
1618
import org.eclipse.openvsx.json.*;
19+
import org.eclipse.openvsx.repositories.ExtensionScanRepository;
1720
import org.eclipse.openvsx.repositories.RepositoryService;
1821
import org.eclipse.openvsx.security.CodedAuthException;
1922
import org.eclipse.openvsx.storage.StorageUtilService;
@@ -54,21 +57,24 @@ public class UserAPI {
5457
private final StorageUtilService storageUtil;
5558
private final LocalRegistryService local;
5659
private final ExtensionService extensions;
60+
private final ExtensionScanRepository scanRepository;
5761

5862
public UserAPI(
5963
RepositoryService repositories,
6064
UserService users,
6165
EclipseService eclipse,
6266
StorageUtilService storageUtil,
6367
LocalRegistryService local,
64-
ExtensionService extensions
68+
ExtensionService extensions,
69+
ExtensionScanRepository scanRepository
6570
) {
6671
this.repositories = repositories;
6772
this.users = users;
6873
this.eclipse = eclipse;
6974
this.storageUtil = storageUtil;
7075
this.local = local;
7176
this.extensions = extensions;
77+
this.scanRepository = scanRepository;
7278
}
7379

7480
@GetMapping(
@@ -208,10 +214,107 @@ public List<ExtensionJson> getOwnExtensions() {
208214
json.setPreview(latest.isPreview());
209215
json.setActive(latest.getExtension().isActive());
210216
json.setFiles(fileUrls.get(latest.getId()));
217+
218+
// Add scan/review status information
219+
enrichWithReviewStatus(json, latest);
220+
211221
return json;
212222
})
213223
.toList();
214224
}
225+
226+
/**
227+
* Add review/scan status information to the extension JSON.
228+
*
229+
* This shows users the current state of their extension in simple terms:
230+
* - "published" - Extension is active and publicly available
231+
* - "under_review" - Extension is being reviewed (validation, scanning, etc.)
232+
* - "rejected" - Extension was blocked (quarantined or rejected)
233+
*/
234+
private void enrichWithReviewStatus(ExtensionJson json, ExtensionVersion extVersion) {
235+
// Look up scan by extension metadata (namespace, name, version, platform)
236+
var ext = extVersion.getExtension();
237+
var scanResult = scanRepository.findFirstByNamespaceNameAndExtensionNameAndExtensionVersionAndTargetPlatformOrderByStartedAtDesc(
238+
ext.getNamespace().getName(),
239+
ext.getName(),
240+
extVersion.getVersion(),
241+
extVersion.getTargetPlatform()
242+
);
243+
244+
if (Boolean.TRUE.equals(json.getActive())) {
245+
// Only mark published if scan result indicates PASSED or no scan result exists (scanning disabled / manual activation)
246+
if (scanResult == null || scanResult.getStatus() == ScanStatus.PASSED) {
247+
json.setReviewStatus("published");
248+
return;
249+
}
250+
}
251+
252+
if (scanResult == null) {
253+
// No scan result found - show as under review
254+
json.setReviewStatus("under_review");
255+
json.setReviewMessage("Your extension is being reviewed.");
256+
return;
257+
}
258+
259+
// Map internal status to simple user-facing status
260+
switch (scanResult.getStatus()) {
261+
case STARTED:
262+
json.setReviewStatus("under_review");
263+
json.setReviewMessage("Your extension is being reviewed.");
264+
break;
265+
case VALIDATING:
266+
json.setReviewStatus("under_review");
267+
json.setReviewMessage("Your extension is being reviewed.");
268+
break;
269+
case SCANNING:
270+
json.setReviewStatus("under_review");
271+
json.setReviewMessage("Your extension is being reviewed.");
272+
break;
273+
case QUARANTINED:
274+
// Check if admin has made a decision on this quarantined scan
275+
var adminDecision = repositories.findAdminScanDecisionByScanId(scanResult.getId());
276+
if (adminDecision != null) {
277+
if (adminDecision.isAllowed()) {
278+
// Admin allowed the extension - show as published if active
279+
if (Boolean.TRUE.equals(json.getActive())) {
280+
json.setReviewStatus("published");
281+
} else {
282+
// Allowed but not yet active (edge case)
283+
json.setReviewStatus("under_review");
284+
json.setReviewMessage("Your extension has been approved and will be published shortly.");
285+
}
286+
} else {
287+
// Admin blocked the extension
288+
json.setReviewStatus("under_review");
289+
json.setReviewMessage("Your extension is being reviewed. Please contact support for details.");
290+
}
291+
} else {
292+
// No admin decision yet - still under review
293+
json.setReviewStatus("under_review");
294+
if (scanResult.getErrorMessage() != null) {
295+
json.setReviewMessage(scanResult.getErrorMessage());
296+
} else {
297+
json.setReviewMessage("Your extension is being reviewed. Please contact support for details.");
298+
}
299+
}
300+
break;
301+
case REJECTED:
302+
json.setReviewStatus("rejected");
303+
if (scanResult.getErrorMessage() != null) {
304+
json.setReviewMessage(scanResult.getErrorMessage());
305+
} else {
306+
json.setReviewMessage("Your extension could not be published. Please contact support for details.");
307+
}
308+
break;
309+
case ERRORED:
310+
json.setReviewStatus("under_review");
311+
json.setReviewMessage("Your extension could not be published. Please contact support for details.");
312+
break;
313+
default:
314+
json.setReviewStatus("under_review");
315+
json.setReviewMessage("Your extension is being reviewed. Please contact support for details.");
316+
}
317+
}
215318

216319
@GetMapping(
217320
path = "/user/extension/{namespaceName}/{extensionName}",

server/src/main/java/org/eclipse/openvsx/admin/AdminService.java

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import org.eclipse.openvsx.mail.MailService;
2323
import org.eclipse.openvsx.migration.HandlerJobRequest;
2424
import org.eclipse.openvsx.repositories.RepositoryService;
25+
import org.eclipse.openvsx.scanning.ExtensionScanPersistenceService;
2526
import org.eclipse.openvsx.search.SearchUtilService;
2627
import org.eclipse.openvsx.storage.StorageUtilService;
2728
import org.eclipse.openvsx.util.*;
@@ -52,6 +53,7 @@ public class AdminService {
5253
private final CacheService cache;
5354
private final JobRequestScheduler scheduler;
5455
private final MailService mail;
56+
private final ExtensionScanPersistenceService scanPersistenceService;
5557

5658
public AdminService(
5759
RepositoryService repositories,
@@ -64,7 +66,8 @@ public AdminService(
6466
StorageUtilService storageUtil,
6567
CacheService cache,
6668
JobRequestScheduler scheduler,
67-
MailService mail
69+
MailService mail,
70+
ExtensionScanPersistenceService scanPersistenceService
6871
) {
6972
this.repositories = repositories;
7073
this.extensions = extensions;
@@ -77,6 +80,7 @@ public AdminService(
7780
this.cache = cache;
7881
this.scheduler = scheduler;
7982
this.mail = mail;
83+
this.scanPersistenceService = scanPersistenceService;
8084
}
8185

8286
@EventListener
@@ -240,6 +244,10 @@ protected ResultJson deleteExtension(ExtensionVersion extVersion, UserData admin
240244
}
241245

242246
private void removeExtensionVersion(ExtensionVersion extVersion) {
247+
// Clean up any pending scan jobs for this extension version
248+
// to prevent "file not found" errors after deletion
249+
scanPersistenceService.deleteScansForExtensionVersion(extVersion.getId());
250+
243251
repositories.findFiles(extVersion).map(RemoveFileJobRequest::new).forEach(scheduler::enqueue);
244252
repositories.deleteFiles(extVersion);
245253
entityManager.remove(extVersion);

0 commit comments

Comments
 (0)