Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
3a81716
Continue the Warden v3 moderation rewrite on its own branch
Villagers654 Sep 15, 2026
9dddd9c
Tighten spacing beside the alpha and beta toggle
Villagers654 Sep 15, 2026
f162316
Show resolved artwork and filenames for external pack entries
Villagers654 Sep 15, 2026
3acfe99
Discover retained status checks for original review cancellations
Villagers654 Sep 15, 2026
ea9e061
Merge independent security fixes from develop into Warden v3
Villagers654 Sep 15, 2026
9b6e3f3
Recover retained status observations through moderator history
Villagers654 Sep 15, 2026
56320cf
Verify moderation cancellation routes through the security filter chain
Villagers654 Sep 15, 2026
6032a11
Merge the login session fix and current develop changes
Villagers654 Sep 15, 2026
4aaf740
Prevent manual rescans from discarding retained remote review references
Villagers654 Sep 15, 2026
884776a
Capture original remote review evidence for retained replacements
Villagers654 Sep 15, 2026
4d780a7
Bind replacement captures to authenticated isolation history
Villagers654 Sep 15, 2026
decd892
Retain signed replacement proposals with fixed request identities
Villagers654 Sep 15, 2026
e568ed5
Stage retained replacements and held admissions atomically
Villagers654 Sep 15, 2026
fa5f014
Verify retained replacement history before subsequent rescans
Villagers654 Sep 15, 2026
7da5700
Discover held replacements and reject missing history pointers
Villagers654 Sep 15, 2026
c8fcb1e
Retain original job observations for replacement admission
Villagers654 Sep 15, 2026
650a916
Sign replacement activation decisions against retained observations
Villagers654 Sep 15, 2026
21fb7b1
Preserve unresolved security blocks through replacement scans
Villagers654 Sep 15, 2026
eb5e10c
Merge independent security checks and current develop changes
Villagers654 Sep 15, 2026
8698b3f
Activate retained replacements through the durable review queue
Villagers654 Sep 15, 2026
5288617
Preserve remote review history through approval and pruning
Villagers654 Sep 15, 2026
12729db
Classify version mutations that require retained review history
Villagers654 Sep 15, 2026
441684d
Retain signed version mutation proposals with fixed evidence
Villagers654 Sep 15, 2026
8331946
Apply retained version edits and removals atomically
Villagers654 Sep 15, 2026
32abaaa
Discover and authenticate retained version mutation history
Villagers654 Sep 15, 2026
c3a9178
Bind grouped version mutations to signed project snapshots
Villagers654 Sep 15, 2026
66f995f
Apply grouped project mutations with atomic retained history
Villagers654 Sep 15, 2026
919c423
Retain signed committed project states for mutation recovery
Villagers654 Sep 15, 2026
23344c9
Discover grouped mutation history from authenticated snapshots
Villagers654 Sep 15, 2026
2d7b609
Compose retained mutations under the shared review budget
Villagers654 Sep 15, 2026
58c4926
Authenticate held version history before subsequent mutations
Villagers654 Sep 15, 2026
dff8c1f
Merge branch 'develop' into warden-v3
Villagers654 Sep 15, 2026
4c5abb0
Bind owner mutations to current account and key authority
Villagers654 Sep 15, 2026
6a5072e
Derive owner mutation permissions from signed version changes
Villagers654 Sep 15, 2026
7c2c63e
Retain review history and artifacts when owners delete versions
Villagers654 Sep 15, 2026
ca8d8be
Retain prior reviews when owners change version context
Villagers654 Sep 15, 2026
d4ce9c5
Retain review identities across draft submission
Villagers654 Sep 15, 2026
0dfb64a
Retain original reviews through replacement uploads
Villagers654 Sep 15, 2026
ce65cda
Retain original job observations for grouped mutations
Villagers654 Sep 15, 2026
192775a
Keep nested review work within the shared deadline
Villagers654 Sep 15, 2026
dbce268
Account for prior work across retained mutation groups
Villagers654 Sep 15, 2026
0c63ebf
Sign held mutation admissions against prior work and scanner identity
Villagers654 Sep 15, 2026
fb958b3
Activate retained mutation requests through the durable review queue
Villagers654 Sep 15, 2026
8e3a728
Authenticate admitted review history through subsequent owner edits
Villagers654 Sep 15, 2026
13d3bc7
Discover held mutation requests in bounded durable pages
Villagers654 Sep 15, 2026
fc24500
Retain bounded automatic admission attempts with durable cooldowns
Villagers654 Sep 15, 2026
4e34955
Coordinate automatic admission from retained original job evidence
Villagers654 Sep 15, 2026
3131d67
Run automatic admission through the bounded background scheduler
Villagers654 Sep 15, 2026
2f79c64
Recover interrupted admission bookkeeping from committed evidence
Villagers654 Sep 15, 2026
11da729
Carry completed job observations across bounded admission passes
Villagers654 Sep 15, 2026
4cb715b
Resume verified admission progress without the failure cooldown
Villagers654 Sep 15, 2026
106cfee
Bound repeated history validation during large admission reviews
Villagers654 Sep 15, 2026
32209f0
Route oversized retained histories to explicit attention
Villagers654 Sep 15, 2026
b3c24f4
Exercise admission at combined retained history limits
Villagers654 Sep 15, 2026
7a37fd4
Retry confirmed admission write conflicts within the shared deadline
Villagers654 Sep 15, 2026
4913f84
Create admission storage before concurrent transactions
Villagers654 Sep 15, 2026
36817a2
Verify full background traversal of retained job evidence
Villagers654 Sep 15, 2026
28c62c1
Verify small projects progress alongside longer review histories
Villagers654 Sep 15, 2026
da1c350
Keep finding history current across expiry and snapshot changes
Villagers654 Sep 15, 2026
54752c2
Add bounded finding navigation while preserving occurrence identity
Villagers654 Sep 15, 2026
8299ca6
Retain prior finding explanations across scoring-only changes
Villagers654 Sep 15, 2026
12cf71f
Verify scoring-independent reasoning rejects stale security evidence
Villagers654 Sep 15, 2026
f983baa
Reset prior reasoning lookup when review context changes
Villagers654 Sep 15, 2026
6440cf6
Group repeated findings without losing individual evidence
Villagers654 Sep 16, 2026
94e2abf
Keep keyboard focus within finding group navigation
Villagers654 Sep 16, 2026
3209655
Restore finding classifications when reuse is held
Villagers654 Sep 16, 2026
ed03975
Build bounded immutable dependency review inventories
Villagers654 Sep 16, 2026
75437d3
Resolve dependency records within bounded database inspections
Villagers654 Sep 16, 2026
9e69e46
Expose dependency inventories in the opened moderation review
Villagers654 Sep 16, 2026
c049414
Verify dependency artifact bytes within bounded resources
Villagers654 Sep 16, 2026
88ebc55
Verify stored dependency files against the opened review inventory
Villagers654 Sep 16, 2026
56e79b8
Reject ambiguous BSON in dependency inspection records
Villagers654 Sep 16, 2026
dd46749
Disable external references button in home page preview
Villagers654 Sep 16, 2026
d48105d
Revert "Disable external references button in home page preview"
Villagers654 Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
4 changes: 4 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ dependencies {
}

tasks.named('test') {
// Explicit database checks must exercise the current database, not cached results.
if (System.getenv('WARDEN_REVIEW_DB_TEST') == 'true' || System.getenv('WARDEN_REPAIR_TX_DB_TEST') == 'true') {
outputs.upToDateWhen { false }
}
useJUnitPlatform()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package net.modtale.config.db;

import java.util.Map;

public interface ArtifactManifestStore {
String put(Map<String,String> entries);
Map<String,String> get(String identity);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package net.modtale.config.db;

import com.mongodb.MongoWriteException;
import net.modtale.model.project.SecurityManifest;
import org.bson.Document;
import org.springframework.data.mongodb.MongoDatabaseFactory;
import org.springframework.data.mapping.MappingException;
import java.util.*;

/** Immutable, content-addressed records; raw driver access avoids recursive mapping conversions. */
public final class MongoArtifactManifestStore implements ArtifactManifestStore {
public static final String COLLECTION = "artifact_manifests";
private final MongoDatabaseFactory database;
public MongoArtifactManifestStore(MongoDatabaseFactory database) { this.database = database; }
@Override public String put(Map<String,String> entries) {
if (!SecurityManifest.valid(entries, false)) throw new MappingException("Invalid artifact manifest");
var snapshot = Collections.unmodifiableMap(new TreeMap<>(entries));
String identity = SecurityManifest.identity(snapshot);
var values = new ArrayList<Document>();
snapshot.forEach((path, hash) -> values.add(new Document("path", path).append("sha256", hash)));
try {
database.getMongoDatabase().getCollection(COLLECTION).insertOne(new Document("_id", identity)
.append("entries", values).append("createdAt", new Date()));
} catch (MongoWriteException duplicate) {
if (duplicate.getError().getCode() != 11000) throw duplicate;
get(identity); // An existing record must really contain the same immutable manifest.
}
return identity;
}
@Override public Map<String,String> get(String identity) {
if (!SecurityManifest.digest(identity)) throw new MappingException("Invalid artifact manifest reference");
Document document = database.getMongoDatabase().getCollection(COLLECTION).find(new Document("_id", identity)).first();
if (document == null) throw new MappingException("Artifact manifest is unavailable");
var evidence = new SecurityEvidenceConverters.Read().convert(new Document("entryHashes", document.get("entries")));
var entries = evidence.entryHashes();
if (!SecurityManifest.valid(entries, false) || !identity.equals(SecurityManifest.identity(entries)))
throw new MappingException("Artifact manifest integrity check failed");
return Collections.unmodifiableMap(new TreeMap<>(entries));
}
}
13 changes: 12 additions & 1 deletion backend/src/main/java/net/modtale/config/db/MongoConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,19 @@
public class MongoConfig {

@Bean
public ArtifactManifestStore artifactManifestStore(org.springframework.data.mongodb.MongoDatabaseFactory database) {
return new MongoArtifactManifestStore(database);
}

@Bean
public MongoCustomConversions mongoCustomConversions(ArtifactManifestStore store) {
return new MongoCustomConversions(List.of(new StringToOAuthProviderConverter(),
new SecurityEvidenceConverters.Write(store), new SecurityEvidenceConverters.Read(store)));
}

public MongoCustomConversions mongoCustomConversions() {
return new MongoCustomConversions(List.of(new StringToOAuthProviderConverter()));
return new MongoCustomConversions(List.of(new StringToOAuthProviderConverter(),
new SecurityEvidenceConverters.Write(), new SecurityEvidenceConverters.Read()));
}

@ReadingConverter
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package net.modtale.config.db;

import net.modtale.model.project.SecurityManifest;
import java.util.*;

/** Existing Map-based consumers still inspect real verified entries before granting clearance. */
final class ReferencedArtifactManifest extends AbstractMap<String,String> {
final ArtifactManifestStore store;
final String identity;
private volatile Map<String,String> loaded;
ReferencedArtifactManifest(ArtifactManifestStore store, String identity) {
if (store == null || !SecurityManifest.digest(identity)) throw new IllegalArgumentException("Invalid artifact manifest reference");
this.store = store; this.identity = identity;
}
private Map<String,String> load() {
var snapshot = loaded;
if (snapshot != null) return snapshot;
synchronized (this) {
if (loaded == null) {
try {
var entries = store.get(identity);
if (!SecurityManifest.valid(entries, false) || !identity.equals(SecurityManifest.identity(entries)))
throw new IllegalStateException("Artifact manifest integrity check failed");
loaded = Collections.unmodifiableMap(new TreeMap<>(entries));
} catch (RuntimeException unavailable) { throw new SecurityManifest.Unavailable(unavailable); }
}
return loaded;
}
}
@Override public Set<Entry<String,String>> entrySet() { return load().entrySet(); }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package net.modtale.config.db;

import java.util.*;
import net.modtale.model.project.ScanResult.SecurityEvidence;
import net.modtale.model.project.SecurityManifest;
import org.bson.Document;
import org.springframework.core.convert.converter.Converter;
import org.springframework.data.convert.ReadingConverter;
import org.springframework.data.convert.WritingConverter;
import org.springframework.data.mapping.MappingException;

public final class SecurityEvidenceConverters {
private SecurityEvidenceConverters() {}

@WritingConverter
public static final class Write implements Converter<SecurityEvidence, Document> {
private final ArtifactManifestStore store;
public Write() { this(null); }
public Write(ArtifactManifestStore store) { this.store = store; }
@Override public Document convert(SecurityEvidence evidence) {
if (store != null && evidence.entryHashes() instanceof ReferencedArtifactManifest reference && reference.store == store)
return header(evidence).append("manifestRef", reference.identity);
if (!SecurityManifest.valid(evidence.entryHashes(), true)) throw new MappingException("Invalid or oversized security manifest");
if (store != null && evidence.entryHashes() != null && !evidence.entryHashes().isEmpty())
return header(evidence).append("manifestRef", store.put(evidence.entryHashes()));
var entries = new ArrayList<Document>();
if (evidence.entryHashes() != null) evidence.entryHashes().forEach((path, hash) ->
entries.add(new Document("path", path).append("sha256", hash)));
return header(evidence).append("entryHashes", entries);
}
private Document header(SecurityEvidence evidence) {
return new Document("policyVersion", evidence.policyVersion())
.append("artifactSha256", evidence.artifactSha256())
.append("contentSha256", evidence.contentSha256())
.append("complete", evidence.complete())
.append("clearanceGranted", evidence.clearanceGranted())
.append("reviewState", evidence.reviewState());
}
}

@ReadingConverter
public static final class Read implements Converter<Document, SecurityEvidence> {
private final ArtifactManifestStore store;
public Read() { this(null); }
public Read(ArtifactManifestStore store) { this.store = store; }
@Override public SecurityEvidence convert(Document source) {
if (source.containsKey("manifestRef")) {
if (source.containsKey("entryHashes") || store == null || !(source.get("manifestRef") instanceof String identity)
|| !SecurityManifest.digest(identity)) throw new MappingException("Invalid artifact manifest reference");
return evidence(source, new ReferencedArtifactManifest(store, identity));
}
var entries = new LinkedHashMap<String, String>();
Object stored = source.get("entryHashes");
if (stored instanceof List<?> list) {
if (list.size() > 20_000) throw new MappingException("Security manifest exceeds entry limit");
for (Object value : list) {
if (!(value instanceof Document entry) || !(entry.get("path") instanceof String path)
|| !(entry.get("sha256") instanceof String hash) || entries.putIfAbsent(path, hash) != null)
throw new MappingException("Invalid security manifest entry");
}
} else if (stored instanceof Map<?, ?> map) {
if (map.size() > 20_000) throw new MappingException("Security manifest exceeds entry limit");
for (var entry : map.entrySet()) {
if (!(entry.getKey() instanceof String path) || !(entry.getValue() instanceof String hash))
throw new MappingException("Invalid legacy security manifest entry");
entries.put(path, hash);
}
} else if (stored != null) throw new MappingException("Invalid security manifest");
if (!SecurityManifest.valid(entries, true)) throw new MappingException("Invalid or oversized security manifest");
return evidence(source, entries);
}
private SecurityEvidence evidence(Document source, Map<String,String> entries) {
return new SecurityEvidence(source.getString("policyVersion"), source.getString("artifactSha256"),
source.getString("contentSha256"), Boolean.TRUE.equals(source.get("complete")),
Boolean.TRUE.equals(source.get("clearanceGranted")), source.getString("reviewState"), entries);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package net.modtale.config.properties;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.bind.DefaultValue;
import java.util.*;

@ConfigurationProperties(prefix="app.warden.repair")
public record AppReviewRepairProperties(@DefaultValue("false") boolean enabled,@DefaultValue("") String activeKey,
Map<String,String> signingKeys,@DefaultValue("1") int concurrency,@DefaultValue("300000") long preparationLifetimeMillis) {
public AppReviewRepairProperties {
signingKeys=signingKeys==null?Map.of():Map.copyOf(signingKeys);
if(enabled) {
if(concurrency<1 || concurrency>2 || preparationLifetimeMillis<1000 || preparationLifetimeMillis>900000
|| activeKey==null || !signingKeys.containsKey(activeKey) || signingKeys.isEmpty() || signingKeys.size()>8)throw invalid();
decode(signingKeys);
}
}
public Map<String,byte[]> decodedKeys(){if(!enabled)throw invalid();return decode(signingKeys);}
private static Map<String,byte[]> decode(Map<String,String> keys) {
var decoded=new HashMap<String,byte[]>();
keys.forEach((id,value)->{
try {
if(id==null || !id.matches("[A-Za-z0-9_-]{1,64}") || value==null || value.length()>172)throw invalid();
byte[] key=Base64.getDecoder().decode(value);
if(key.length<32 || key.length>128 || !Base64.getEncoder().encodeToString(key).equals(value))throw invalid();
decoded.put(id,key);
} catch(IllegalArgumentException invalid){throw invalid();}
});return Map.copyOf(decoded);
}
private static IllegalArgumentException invalid(){return new IllegalArgumentException("Invalid review repair settings");}
@Override public String toString(){return "ReviewRepairSettings[enabled="+enabled+"]";}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ public record AppWardenProperties(
@DefaultValue("http://localhost:8081") String url,
@DefaultValue("") String apiKey,
@DefaultValue("true") boolean enabled,
@DefaultValue("3") int maxAttempts,
@DefaultValue("75") long requestTimeoutSeconds
@DefaultValue("1") int maxAttempts,
@DefaultValue("600") long requestTimeoutSeconds
) {
}
Loading
Loading