feat(security): add Security tab to repo navigation #541
@@ -1969,6 +1969,7 @@
|
||||
"repo.signing.wont_sign.approved": "The merge will not be signed as the PR is not approved.",
|
||||
"repo.ext_wiki": "Access to External Wiki",
|
||||
"repo.ext_wiki.desc": "Link to an external wiki.",
|
||||
"repo.security": "Security",
|
||||
"repo.wiki": "Wiki",
|
||||
"repo.wiki.welcome": "Welcome to the Wiki.",
|
||||
"repo.wiki.welcome_desc": "The wiki lets you write and share documentation with collaborators.",
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package repo
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
|
||||
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
|
||||
security_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/security"
|
||||
)
|
||||
|
||||
const tplRepoSecurity templates.TplName = "repo/security"
|
||||
|
||||
// Security renders the repo-level security tab showing alerts and scan controls.
|
||||
func Security(ctx *context.Context) {
|
||||
ctx.Data["Title"] = ctx.Tr("repo.security")
|
||||
ctx.Data["PageIsSecurity"] = true
|
||||
|
||||
repoID := ctx.Repo.Repository.ID
|
||||
|
||||
cfg, err := security_model.GetScannerConfig(ctx, repoID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetScannerConfig", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["ScannerConfig"] = cfg
|
||||
|
||||
alerts, err := security_model.GetAllAlerts(ctx, repoID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAllAlerts", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["SecurityAlerts"] = alerts
|
||||
|
||||
counts, err := security_model.GetAlertCountsByRepo(ctx, repoID)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAlertCountsByRepo", err)
|
||||
return
|
||||
}
|
||||
ctx.Data["AlertCounts"] = counts
|
||||
|
||||
ctx.HTML(http.StatusOK, tplRepoSecurity)
|
||||
}
|
||||
|
||||
// SecurityScanNow triggers an immediate scan from the security tab.
|
||||
func SecurityScanNow(ctx *context.Context) {
|
||||
commit := ctx.Repo.Commit
|
||||
if commit == nil {
|
||||
ctx.Flash.Error("No commits found")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/security")
|
||||
return
|
||||
}
|
||||
|
||||
security_service.ScanOnPush(ctx, ctx.Repo.Repository, commit)
|
||||
ctx.Flash.Success(ctx.Tr("repo.settings.security_scan_complete"))
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/security")
|
||||
}
|
||||
|
||||
// SecurityAlertUpdate changes alert status from the security tab.
|
||||
func SecurityAlertUpdateTab(ctx *context.Context) {
|
||||
id := ctx.PathParamInt64("id")
|
||||
status := security_model.AlertStatus(ctx.FormString("status"))
|
||||
|
||||
if status != security_model.AlertStatusResolved && status != security_model.AlertStatusDismissed {
|
||||
status = security_model.AlertStatusDismissed
|
||||
}
|
||||
|
||||
alert, err := security_model.GetAlertByID(ctx, id)
|
||||
if err != nil {
|
||||
ctx.ServerError("GetAlertByID", err)
|
||||
return
|
||||
}
|
||||
if alert.RepoID != ctx.Repo.Repository.ID {
|
||||
ctx.NotFound(nil)
|
||||
return
|
||||
}
|
||||
|
||||
if err := security_model.UpdateAlertStatus(ctx, id, status, ctx.Doer.ID); err != nil {
|
||||
ctx.ServerError("UpdateAlertStatus", err)
|
||||
return
|
||||
}
|
||||
|
||||
ctx.Flash.Success("Alert updated")
|
||||
ctx.Redirect(ctx.Repo.RepoLink + "/security")
|
||||
}
|
||||
@@ -1677,6 +1677,13 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
|
||||
})
|
||||
// end "/{username}/{reponame}/wiki"
|
||||
|
||||
m.Group("/{username}/{reponame}/security", func() {
|
||||
m.Get("", repo.Security)
|
||||
m.Post("/scan", reqRepoAdmin, repo.SecurityScanNow)
|
||||
m.Post("/alert/{id}", reqRepoAdmin, repo.SecurityAlertUpdateTab)
|
||||
}, reqSignIn, context.RepoAssignment, reqRepoAdmin)
|
||||
// end "/{username}/{reponame}/security"
|
||||
|
||||
m.Group("/{username}/{reponame}/activity", func() {
|
||||
// activity has its own permission checks
|
||||
m.Get("", repo.Activity)
|
||||
|
||||
@@ -122,6 +122,15 @@
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if and .Permission.IsAdmin .IsSigned}}
|
||||
<a class="{{if .PageIsSecurity}}active {{end}}item" href="{{.RepoLink}}/security">
|
||||
{{svg "octicon-shield"}} {{ctx.Locale.Tr "repo.security"}}
|
||||
{{if .SecurityAlertCount}}
|
||||
<span class="ui small label red">{{CountFmt .SecurityAlertCount}}</span>
|
||||
{{end}}
|
||||
</a>
|
||||
{{end}}
|
||||
|
||||
{{if .Permission.CanRead ctx.Consts.RepoUnitTypePackages}}
|
||||
<a href="{{.RepoLink}}/packages" class="{{if .IsPackagesPage}}active {{end}}item">
|
||||
{{svg "octicon-package"}} {{ctx.Locale.Tr "packages.title"}}
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
{{template "base/head" .}}
|
||||
<div role="main" aria-label="{{.Title}}" class="page-content repository security">
|
||||
{{template "repo/header" .}}
|
||||
<div class="ui container">
|
||||
<div class="tw-flex tw-justify-between tw-items-center tw-mb-4">
|
||||
<h2>{{svg "octicon-shield" 20 "tw-mr-2"}}{{ctx.Locale.Tr "repo.security"}}</h2>
|
||||
{{if .Permission.IsAdmin}}
|
||||
<div class="tw-flex tw-gap-2">
|
||||
<form method="post" action="{{.RepoLink}}/security/scan" class="tw-inline">
|
||||
{{.CsrfTokenHtml}}
|
||||
<button class="ui small primary button" type="submit">{{svg "octicon-sync" 14}} {{ctx.Locale.Tr "repo.settings.security_scan_now"}}</button>
|
||||
</form>
|
||||
<a class="ui small button" href="{{.RepoLink}}/settings/security">{{svg "octicon-gear" 14}} Settings</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
|
||||
{{if .AlertCounts}}
|
||||
<div class="tw-flex tw-gap-3 tw-mb-4">
|
||||
{{range $sev, $count := .AlertCounts}}
|
||||
<div class="ui {{if eq $sev "critical"}}red{{else if eq $sev "high"}}orange{{else if eq $sev "medium"}}yellow{{else if eq $sev "low"}}blue{{else}}grey{{end}} label">
|
||||
{{$sev}}: {{$count}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .SecurityAlerts}}
|
||||
<table class="ui compact table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ctx.Locale.Tr "repo.settings.security_severity"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.settings.security_scanner_type"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.settings.security_finding"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.settings.security_file"}}</th>
|
||||
<th>{{ctx.Locale.Tr "repo.settings.security_status"}}</th>
|
||||
{{if .Permission.IsAdmin}}<th></th>{{end}}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .SecurityAlerts}}
|
||||
<tr {{if ne .Status "active"}}class="tw-opacity-50"{{end}}>
|
||||
<td>
|
||||
<span class="ui mini {{if eq .Severity "critical"}}red{{else if eq .Severity "high"}}orange{{else if eq .Severity "medium"}}yellow{{else if eq .Severity "low"}}blue{{else}}grey{{end}} label">
|
||||
{{.Severity}}
|
||||
</span>
|
||||
</td>
|
||||
<td>{{.Scanner}}</td>
|
||||
<td>
|
||||
<strong>{{.Title}}</strong>
|
||||
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if .FilePath}}
|
||||
<a href="{{$.RepoLink}}/src/branch/{{$.BranchName}}/{{.FilePath}}{{if .LineNumber}}#L{{.LineNumber}}{{end}}">
|
||||
<code class="tw-text-xs">{{.FilePath}}{{if .LineNumber}}:{{.LineNumber}}{{end}}</code>
|
||||
</a>
|
||||
{{end}}
|
||||
</td>
|
||||
<td>
|
||||
{{if eq .Status "active"}}
|
||||
<span class="ui mini red label">Active</span>
|
||||
{{else if eq .Status "resolved"}}
|
||||
<span class="ui mini green label">Resolved</span>
|
||||
{{else}}
|
||||
<span class="ui mini grey label">Dismissed</span>
|
||||
{{end}}
|
||||
</td>
|
||||
{{if $.Permission.IsAdmin}}
|
||||
<td class="tw-text-right">
|
||||
{{if eq .Status "active"}}
|
||||
<form method="post" action="{{$.RepoLink}}/security/alert/{{.ID}}" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="status" value="resolved">
|
||||
<button class="ui tiny green icon button" type="submit" title="Resolve">{{svg "octicon-check" 14}}</button>
|
||||
</form>
|
||||
<form method="post" action="{{$.RepoLink}}/security/alert/{{.ID}}" class="tw-inline">
|
||||
{{$.CsrfTokenHtml}}
|
||||
<input type="hidden" name="status" value="dismissed">
|
||||
<button class="ui tiny grey icon button" type="submit" title="Dismiss">{{svg "octicon-x" 14}}</button>
|
||||
</form>
|
||||
{{end}}
|
||||
</td>
|
||||
{{end}}
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{else}}
|
||||
<div class="ui segment">
|
||||
<div class="empty-placeholder">
|
||||
<p>{{svg "octicon-shield-check" 48}}</p>
|
||||
<p>{{ctx.Locale.Tr "repo.settings.security_no_alerts"}}</p>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{template "base/footer" .}}
|
||||
Reference in New Issue
Block a user