Skip to content

Commit 429339e

Browse files
authored
fix: surface actionable validation step messages in push detail view (#246)
## Summary - **Fixes #244**: `scanDiff` step content was showing the branch ref (`ref: refs/heads/...`) instead of the blocked pattern and matching file — `BlockedContentDiffCheck` was producing violations with null `formattedDetail`, and `DiffScanningHook` was hardcoding the ref name as content - **scanSecrets**: both hook and filter now append a remediation hint (rotate + scrub history) to each finding so the dashboard gives actionable next steps - **identityVerification WARN mode**: now emits per-violation sideband warnings to the git client terminal so developers see the mismatch even when the push isn't blocked - **URL rule step consistency**: `RepositoryUrlRuleHook` now uses step name `checkUrlRules` (not class name); fixes a duplicate step bug where both `validationContext.addIssue` and `pushContext.addStep(FAIL)` were called, producing two entries in the push detail view; wording aligned with `UrlRuleAggregateFilter` - **URL rules page display fixed**: `Repos.tsx` was using the old `slug`/`owner`/`name` field shape; updated to `target`/`value`/`matchType` from the `34aebb9` backend refactor — every rule was showing as "any: *" and new rules created via UI were saving with `value=null` (never matching) - **Admin connectivity crash fixed**: `SpringWebConfig` sets `NON_NULL` Jackson inclusion globally, so null `tls`/`http` fields are omitted from JSON rather than serialised as `null`; the frontend strict `=== null` check fell through to `tls.status` and crashed — changed to `== null` throughout and updated types to `| undefined` - **Provider display**: all provider dropdowns (Add Rule, Add Permission, Add SCM identity) and provider labels throughout the UI were showing `host` (e.g. `github.com`) instead of `name` (e.g. `github`); fixed across `Repos.tsx`, `UserDetail.tsx`, `Profile.tsx`; clone URL construction switched to use `provider.proxyPath` from the API response; `Repos.tsx` provider state typed as `Provider[]` instead of an ad-hoc inline type - **Terminology**: `createAccessRule`/`deleteAccessRule` → `createUrlRule`/`deleteUrlRule` in `api.ts` and `Repos.tsx`; "Access type" label → "Rule type" in the add-rule modal ## Test plan - [ ] Unit tests: `BlockedContentDiffCheckTest` — assertions for `formattedDetail` content and deduplication with first matching line - [ ] Unit tests: `ScanDiffFilterTest` — assertion that `errorMessage` contains the blocked pattern and `content` includes the matching line - [ ] Server e2e tests: all 9/9 pass (proxy push flows, URL rule enforcement, secret scanning) - [ ] Dashboard LDAP e2e test failure is pre-existing and unrelated (timing/container issue on corporate network; passes on CI) - [ ] Manual: URL Rules tab shows correct target/value/matchType for each rule; new rules created via UI save and evaluate correctly - [ ] Manual: Admin → Provider Connectivity no longer crashes when tls/http are absent (HTTP provider or TCP failure path) - [ ] Manual: All provider dropdowns show friendly name (e.g. "github") not hostname (e.g. "github.com"); clone URL in active repos view is correct 🤖 Generated with [Claude Code](https://claude.com/claude-code)
2 parents 68ac9c9 + 8cba7b5 commit 429339e

19 files changed

Lines changed: 173 additions & 106 deletions

git-proxy-java-core/src/main/java/org/finos/gitproxy/git/DiffScanningHook.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
6161

6262
if (!violations.isEmpty()) {
6363
for (Violation violation : violations) {
64-
validationContext.addIssue("scanDiff", violation.reason(), "ref: " + cmd.getRefName());
64+
validationContext.addIssue("scanDiff", violation.reason(), violation.formattedDetail());
6565
logs.add("FAIL: " + violation.reason());
6666
}
6767
anyFailed = true;

git-proxy-java-core/src/main/java/org/finos/gitproxy/git/GitClientUtils.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ private static String convertSymbolsToPlain(String content) {
191191
return content;
192192
}
193193

194-
private static String stripColors(String content) {
194+
public static String stripColors(String content) {
195195
for (var color : AnsiColor.values()) {
196196
content = content.replace(color.getValue(), "");
197197
}

git-proxy-java-core/src/main/java/org/finos/gitproxy/git/IdentityVerificationHook.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,12 +126,18 @@ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
126126
validationContext.addIssue(
127127
STEP_NAME, "Commit identity does not match push user " + user.getUsername(), detail);
128128
} else {
129-
// WARN mode — push proceeds; violations are surfaced via the validation summary, not
130-
// immediate per-message streaming, so they appear in the correct order with context.
129+
// WARN mode — push proceeds but developer is warned at the terminal and in the dashboard.
131130
log.warn(
132131
"Identity verification warnings for push user '{}': {} mismatch(es)",
133132
user.getUsername(),
134133
violations.size());
134+
rp.sendMessage(GitClientUtils.color(
135+
YELLOW,
136+
sym(WARNING) + " Identity warning: " + violations.size() + " commit email(s) not registered to "
137+
+ user.getUsername()));
138+
for (String v : violations) {
139+
rp.sendMessage(" " + v);
140+
}
135141
pushContext.addStep(PushStep.builder()
136142
.stepName(STEP_NAME)
137143
.stepOrder(ORDER)

git-proxy-java-core/src/main/java/org/finos/gitproxy/git/PushStorePersistenceHook.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ public class PushStorePersistenceHook {
4545
* sort validation steps in the same order as proxy mode.
4646
*/
4747
private static final Map<String, Integer> HOOK_STEP_ORDER = Map.of(
48+
"checkUrlRules", 100,
4849
"checkAuthorEmails", 2100,
4950
"checkCommitMessages", 2200,
5051
"scanDiff", 2300,
@@ -142,7 +143,7 @@ public PreReceiveHook validationResultHook(ValidationContext validationContext)
142143
.stepName(issue.hookName())
143144
.stepOrder(stepOrder)
144145
.status(StepStatus.FAIL)
145-
.content(issue.detail())
146+
.content(GitClientUtils.stripColors(issue.detail()))
146147
.errorMessage(issue.summary())
147148
.build());
148149
fallbackOrder++;

git-proxy-java-core/src/main/java/org/finos/gitproxy/git/RepositoryUrlRuleHook.java

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,12 @@ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
4444
String repoSlug = pushContext.getRepoSlug();
4545
if (repoSlug == null || repoSlug.isBlank()) {
4646
log.warn("No repoSlug in push context — cannot evaluate URL rules, blocking push (fail-closed)");
47-
blockPush(rp, commands, "Repository path unavailable");
47+
blockPush(
48+
rp,
49+
commands,
50+
"Repository path unavailable",
51+
"Push Blocked - Repository Not Allowed",
52+
"Repository path could not be determined. Contact an administrator.");
4853
return;
4954
}
5055

@@ -59,44 +64,52 @@ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
5964
switch (result) {
6065
case UrlRuleEvaluator.Result.Denied d -> {
6166
log.debug("Push blocked by deny rule: {}", d.ruleId());
62-
blockPush(rp, commands, "Repository denied by access rule");
67+
blockPush(
68+
rp,
69+
commands,
70+
"Repository blocked by deny rule",
71+
"Push Blocked - Repository Denied",
72+
"Pushes to this repository are not permitted.\n\n"
73+
+ "This repository has been explicitly denied by an administrator.");
6374
}
6475
case UrlRuleEvaluator.Result.Allowed a -> {
6576
log.debug("Push allowed by rule: {}", a.ruleId());
6677
recordPass();
6778
}
6879
case UrlRuleEvaluator.Result.NotAllowed n -> {
6980
log.debug("Push blocked — no rule matched");
70-
blockPush(rp, commands, "Repository is not in the allow list");
81+
blockPush(
82+
rp,
83+
commands,
84+
"Repository not in allow list",
85+
"Push Blocked - Repository Not Allowed",
86+
"Pushes to this repository are not permitted.\n\n"
87+
+ "Contact an administrator to add this repository to the allow rules.");
7188
}
7289
}
7390
}
7491

75-
private void blockPush(ReceivePack rp, Collection<ReceiveCommand> commands, String reason) {
76-
String detail = GitClientUtils.format(
77-
sym(NO_ENTRY) + " Push Blocked - Repository Not Allowed",
78-
sym(CROSS_MARK)
79-
+ " "
80-
+ reason
81-
+ ".\n\nContact an administrator to add this repository to the allow rules.",
82-
RED,
83-
null);
92+
private void blockPush(
93+
ReceivePack rp, Collection<ReceiveCommand> commands, String reason, String title, String message) {
94+
String detail =
95+
GitClientUtils.format(sym(NO_ENTRY) + " " + title, sym(CROSS_MARK) + " " + message, RED, null);
8496
if (validationContext != null) {
85-
validationContext.addIssue("RepositoryUrlRuleHook", reason, detail);
97+
validationContext.addIssue("checkUrlRules", reason, detail);
98+
// PushStorePersistenceHook creates the FAIL step from the issue; don't also add it to pushContext
8699
} else {
87100
rp.sendMessage(detail);
88101
for (ReceiveCommand cmd : commands) {
89102
if (cmd.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
90103
cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, reason);
91104
}
92105
}
106+
pushContext.addStep(PushStep.builder()
107+
.stepName("checkUrlRules")
108+
.stepOrder(ORDER)
109+
.status(StepStatus.FAIL)
110+
.content(reason)
111+
.build());
93112
}
94-
pushContext.addStep(PushStep.builder()
95-
.stepName("checkUrlRules")
96-
.stepOrder(ORDER)
97-
.status(StepStatus.FAIL)
98-
.content(reason)
99-
.build());
100113
}
101114

102115
private void recordPass() {

git-proxy-java-core/src/main/java/org/finos/gitproxy/git/SecretScanningHook.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ public class SecretScanningHook implements GitProxyHook {
3434

3535
private static final int ORDER = 340;
3636
private static final String STEP_NAME = "scanSecrets";
37+
private static final String REMEDIATION_HINT =
38+
"→ Rotate any exposed credentials and remove the secret from your commit history before pushing.";
3739

3840
private final SecretScanConfig config;
3941
private final ValidationContext validationContext;
@@ -68,15 +70,15 @@ public void onPreReceive(ReceivePack rp, Collection<ReceiveCommand> commands) {
6870

6971
for (GitleaksRunner.Finding f : result.get()) {
7072
String msg = f.toMessage();
71-
allViolations.add(new Violation(msg, msg, sym(CROSS_MARK) + " " + msg));
73+
allViolations.add(new Violation(msg, msg, sym(CROSS_MARK) + " " + msg + "\n" + REMEDIATION_HINT));
7274
}
7375
}
7476

7577
if (scannerUnavailable) {
7678
if (config.isEnabled()) {
7779
String msg = "Secret scanning failed — scanner error or unavailable. "
7880
+ "Push blocked because secret-scan is enabled. Check server logs for details.";
79-
allViolations.add(new Violation(msg, msg, sym(CROSS_MARK) + " " + msg));
81+
allViolations.add(new Violation(msg, msg, sym(CROSS_MARK) + " " + msg + "\n" + REMEDIATION_HINT));
8082
} else {
8183
pushContext.addStep(PushStep.builder()
8284
.stepName(STEP_NAME)

git-proxy-java-core/src/main/java/org/finos/gitproxy/servlet/filter/CheckHiddenCommitsFilter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ public void doHttpFilter(HttpServletRequest request, HttpServletResponse respons
109109

110110
} catch (Exception e) {
111111
log.warn("Skipping hidden commits check: {}", e.getMessage());
112-
recordStep(request, StepStatus.SKIPPED, null, e.getMessage());
112+
recordStep(request, StepStatus.SKIPPED, "", e.getMessage());
113113
}
114114
}
115115

git-proxy-java-core/src/main/java/org/finos/gitproxy/servlet/filter/GitProxyFilter.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.eclipse.jgit.transport.SideBandOutputStream;
1919
import org.finos.gitproxy.db.model.PushStep;
2020
import org.finos.gitproxy.db.model.StepStatus;
21+
import org.finos.gitproxy.git.GitClientUtils;
2122
import org.finos.gitproxy.git.GitRequestDetails;
2223
import org.finos.gitproxy.git.HttpOperation;
2324
// import org.springframework.core.Ordered;
@@ -130,7 +131,7 @@ default void doFilter(ServletRequest request, ServletResponse response, FilterCh
130131
// so we must not overwrite that with a spurious PASS.
131132
int stepsAfter = details != null ? details.getSteps().size() : 0;
132133
if (stepsAfter == stepsBefore) {
133-
recordStep(httpRequest, StepStatus.PASS, null, null);
134+
recordStep(httpRequest, StepStatus.PASS, "", "");
134135
}
135136
}
136137
chain.doFilter(request, response);
@@ -222,7 +223,7 @@ default void recordStep(HttpServletRequest request, StepStatus status, String re
222223
.stepName(getStepName())
223224
.stepOrder(this.getOrder())
224225
.status(status)
225-
.content(content)
226+
.content(GitClientUtils.stripColors(content))
226227
.blockedMessage(status == StepStatus.BLOCKED ? reason : null)
227228
.errorMessage(status == StepStatus.FAIL ? reason : null)
228229
.build();

git-proxy-java-core/src/main/java/org/finos/gitproxy/servlet/filter/ScanDiffFilter.java

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
package org.finos.gitproxy.servlet.filter;
22

3-
import static org.finos.gitproxy.git.GitClientUtils.AnsiColor.*;
4-
import static org.finos.gitproxy.git.GitClientUtils.SymbolCodes.*;
5-
import static org.finos.gitproxy.git.GitClientUtils.sym;
63
import static org.finos.gitproxy.servlet.GitProxyServlet.GIT_REQUEST_ATTR;
74

85
import jakarta.servlet.http.HttpServletRequest;
@@ -11,15 +8,13 @@
118
import java.util.List;
129
import java.util.Set;
1310
import java.util.function.Supplier;
14-
import java.util.stream.Collectors;
1511
import lombok.extern.slf4j.Slf4j;
1612
import org.eclipse.jgit.lib.Repository;
1713
import org.finos.gitproxy.config.DiffScanConfig;
1814
import org.finos.gitproxy.db.model.PushStep;
1915
import org.finos.gitproxy.db.model.StepStatus;
2016
import org.finos.gitproxy.git.CommitInspectionService;
2117
import org.finos.gitproxy.git.DiffGenerationHook;
22-
import org.finos.gitproxy.git.GitClientUtils;
2318
import org.finos.gitproxy.git.GitRequestDetails;
2419
import org.finos.gitproxy.git.HttpOperation;
2520
import org.finos.gitproxy.provider.GitProxyProvider;
@@ -101,23 +96,17 @@ public void doHttpFilter(HttpServletRequest request, HttpServletResponse respons
10196

10297
if (!violations.isEmpty()) {
10398
log.warn("Diff scan found {} violation(s)", violations.size());
104-
String title = sym(NO_ENTRY) + " Push Blocked - Diff Contains Blocked Content";
105-
String violationList = violations.stream()
106-
.map(v -> sym(CROSS_MARK) + " " + v.reason())
107-
.collect(Collectors.joining("\n"));
108-
String message = "Diff content contains blocked patterns:\n\n" + violationList;
109-
recordIssue(
110-
request,
111-
"Diff contains blocked content",
112-
GitClientUtils.color(RED, GitClientUtils.format(title, message, RED, null)));
99+
for (Violation v : violations) {
100+
recordIssue(request, v.reason(), v.formattedDetail());
101+
}
113102
} else {
114103
log.debug("Diff scan passed for {}..{}", fromCommit, toCommit);
115-
recordStep(request, StepStatus.PASS, null, null);
104+
recordStep(request, StepStatus.PASS, "", "");
116105
}
117106

118107
} catch (Exception e) {
119108
log.warn("Skipping diff scan for push {}..{}: {}", fromCommit, toCommit, e.getMessage());
120-
recordStep(request, StepStatus.SKIPPED, null, e.getMessage());
109+
recordStep(request, StepStatus.SKIPPED, "", e.getMessage());
121110
}
122111
}
123112
}

git-proxy-java-core/src/main/java/org/finos/gitproxy/servlet/filter/SecretScanningFilter.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@
3333
public class SecretScanningFilter extends AbstractGitProxyFilter {
3434

3535
private static final int ORDER = 340;
36+
private static final String REMEDIATION_HINT =
37+
"→ Rotate any exposed credentials and remove the secret from your commit history before pushing.";
3638

3739
private final Supplier<SecretScanConfig> configSupplier;
3840
private final GitleaksRunner runner;
@@ -114,7 +116,7 @@ public void doHttpFilter(HttpServletRequest request, HttpServletResponse respons
114116
log.warn("Secret scan found {} finding(s)", findings.size());
115117
for (GitleaksRunner.Finding f : findings) {
116118
String msg = f.toMessage();
117-
recordIssue(request, msg, sym(CROSS_MARK) + " " + msg);
119+
recordIssue(request, msg, sym(CROSS_MARK) + " " + msg + "\n" + REMEDIATION_HINT);
118120
}
119121
}
120122
}

0 commit comments

Comments
 (0)