Compare commits

...

34 Commits

Author SHA1 Message Date
jmiller f3ce51d629 Merge pull request 'chore: final version update' (#560) from chore/final-version into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-07 02:21:06 +00:00
Jonathan Miller 1f505b48c7 chore: update wiki version to v1.26.1-moko.06.11.01
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m49s
2026-06-06 21:20:37 -05:00
jmiller afda7abcbe Merge pull request 'fix(settings): remove duplicate description from manifest' (#559) from fix/manifest-desc-dedup into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-07 02:08:31 +00:00
Jonathan Miller 798d9c3ae0 fix(settings): remove duplicate description from manifest page
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Failing after 9s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m40s
Description is already managed in repo general settings. The manifest
now reads it from the repository object instead of a separate form field.
2026-06-06 21:07:39 -05:00
jmiller e0f22dd397 Merge pull request 'chore: wiki version update' (#558) from chore/wiki-version-update into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-07 01:57:10 +00:00
Jonathan Miller ecda05aa46 chore: update wiki version to v1.26.1-moko.06.11.00 and roadmap
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m26s
2026-06-06 20:56:25 -05:00
jmiller 9eadade2f4 Merge pull request 'feat: Issue Types settings + MCP SSE + npm auto-publish' (#557) from feat/type-settings-mcp-sse-npm into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-06-07 00:53:45 +00:00
Jonathan Miller 2cc4f7c047 feat: org settings for Issue Types + MCP SSE hosted + npm auto-publish
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m9s
- Org settings page for managing Issue Type definitions (CRUD)
- MCP SSE endpoint deployed at git.mokoconsulting.tech/mcp/
- npm auto-publish workflow on MCP source changes to main
2026-06-06 19:52:55 -05:00
jmiller 74a5fe2b80 Merge pull request 'fix(auth): login form with OAuth on all error pages' (#556) from fix/error-pages-login into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
2026-06-06 23:26:43 +00:00
Jonathan Miller 50c472991a fix(auth): add login form with OAuth to all error pages
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Failing after 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m33s
- 403: OAuth buttons moved below password form (matches regular login)
- 404: Login form with OAuth added for unauthenticated users
- Both pages load OAuth2 providers and show Sign In with Google etc.
2026-06-06 18:25:51 -05:00
jmiller 7dabf844a8 Merge pull request 'fix(auth): show OAuth providers on 403 login form' (#547) from fix/403-oauth-login into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-06 23:16:41 +00:00
Jonathan Miller 7d03541201 fix(auth): show OAuth providers on 403 login form
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m13s
The 403 Access Denied page login form now shows OAuth2 provider
buttons (Sign in with Google, etc.) alongside the username/password
form. Previously only showed password login even when OAuth was
configured.
2026-06-06 18:15:25 -05:00
jmiller d4a2c33c37 Merge pull request 'chore: changelog + MCP type/security tools' (#546) from chore/changelog-mcp-update into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-06 22:51:55 +00:00
Jonathan Miller e59290802a chore: update changelog and MCP with type/security tools
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m32s
- Add v1.26.1-moko.06.10 changelog entry with all features
- Add gitea_org_issue_types_list, gitea_issue_set_type, gitea_security_alerts to MCP
- Add type_id param to issue create
2026-06-06 17:51:09 -05:00
jmiller 1d857d8205 Merge pull request 'chore: update wiki' (#544) from chore/wiki-mcp-update into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 1s
Generic: Repo Health / Site Health (push) Has been skipped
2026-06-06 22:26:54 +00:00
Jonathan Miller 4f9aeb7b85 chore: update wiki - version, first-class fields, security, roadmap
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m43s
- Update version to v1.26.1-moko.06.10.00
- Document 13 default statuses including Pending states
- Document status replacing close button in comment form
- Add Security scanning, MCP, Wiki folders to features list
- Update roadmap with completed and planned items
- Add features/ folder link to pages table
2026-06-06 17:26:19 -05:00
jmiller 1178eaec62 Merge pull request 'feat(issues): first-class Type field + list badges' (#543) from feat/issue-type-first-class into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
2026-06-06 22:13:40 +00:00
Jonathan Miller dd1454c3cf feat(issues): first-class Type field + status/priority/type badges in issue list
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m10s
- IssueTypeDef model with auto-seed defaults (Bug, Feature, Enhancement, Task, Documentation, Security)
- Migration v350 adding issue_type_def table + type_id on issues
- Type dropdown in issue sidebar
- Type, Priority, Status colored badges in issue list view
- Status/Priority/Type definitions loaded in issue list handler
2026-06-06 17:12:44 -05:00
jmiller c539bed4d3 Merge pull request 'fix(ui): dashboard issue count badges' (#542) from fix/dashboard-badges into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-06 21:56:28 +00:00
Jonathan Miller 135b37edf1 fix(ui): use badge labels instead of strong tags for dashboard issue counts
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 4s
Universal: PR Check / Validate PR (pull_request) Failing after 8s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m20s
The sidebar counts (In your repositories, Assigned to you, etc.)
were rendering as bold text concatenated with the label. Now uses
ui small label spans for proper badge styling.
2026-06-06 16:54:26 -05:00
jmiller 48cf445e79 Merge pull request 'feat(security): add Security tab to repo navigation' (#541) from feat/508-security-scanner into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 3s
2026-06-06 21:36:36 +00:00
Jonathan Miller 72708b5a99 feat(security): add Security tab to repo navigation (#508)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 7s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m19s
Add a top-level Security tab in the repo header (visible to admins
only) showing alerts, scan controls, and severity badges. Links to
settings page for scanner configuration. Alert file paths link
directly to the source file.
2026-06-06 16:35:55 -05:00
jmiller 948860e8ac Merge pull request 'feat(security): built-in security scanning platform (#508)' (#540) from feat/508-security-scanner into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-06-06 21:24:11 +00:00
Jonathan Miller f7c1904625 feat(security): built-in security scanning platform (#508)
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m44s
Add a pluggable security scanning framework with secret detection
as the first scanner module. Scans run on push to default branch
and on-demand via the Security settings page.

Includes:
- Scanner interface for pluggable scanner types
- Secret scanner with 15 built-in patterns (AWS, GitHub, Stripe, etc.)
- SecurityAlert model with fingerprint-based dedup
- SecurityScannerConfig per-repo settings
- Migration v349 for security tables
- Repo settings Security page with alerts table
- Scan Now button for on-demand scanning
- Alert resolve/dismiss actions
- Push-time scanning in post-receive hook
2026-06-06 16:23:08 -05:00
jmiller e3b2df4aac Merge pull request 'fix(wiki): folder listing template' (#539) from feat/79-wiki-folders into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-06 20:52:19 +00:00
Jonathan Miller 6cd4a19ed6 fix(wiki): render folder listing template instead of start page
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m4s
When navigating to a wiki folder with no index file, Wiki() handler
now renders the wiki view template with IsWikiFolder flag instead of
falling back to the empty wiki start page.
2026-06-06 15:51:24 -05:00
jmiller 4d73c6a939 Merge pull request 'fix(wiki): directory check before raw redirect' (#538) from feat/79-wiki-folders into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-06 20:35:25 +00:00
Jonathan Miller df91ed2aac fix(wiki): check directory before file lookup to prevent raw redirect
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m0s
Directory paths were being found by wikiEntryByName as non-.md entries,
triggering a redirect to /wiki/raw/. Now checks for directories first
and handles index file lookup before the file/raw detection.
2026-06-06 15:34:49 -05:00
jmiller 8639d85fe7 Merge pull request 'fix(wiki): type mismatch in folder listing' (#537) from feat/79-wiki-folders into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
2026-06-06 20:28:57 +00:00
Jonathan Miller e7a79d973e fix(wiki): type mismatch in folder listing
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Access control (push) Successful in 2s
Generic: Repo Health / Site Health (push) Has been skipped
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Branch Policy Check / Verify merge target (pull_request) Successful in 2s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Site Health (pull_request) Has been skipped
PR RC Release / Build RC Release (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
Universal: PR Check / Validate PR (pull_request) Failing after 5s
Branch Cleanup / Delete merged branch (pull_request) Successful in 1s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 54s
2026-06-06 15:28:20 -05:00
jmiller 6c5394107e Merge pull request 'fix(wiki): proper display names in tree' (#536) from feat/79-wiki-folders into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-06-06 20:25:41 +00:00
Jonathan Miller 0a158e9ec3 fix(wiki): use GitPathToWebPath for proper display names in tree
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
Generic: Repo Health / Access control (pull_request) Successful in 2s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 57s
Fixes the .- dash marker showing in sidebar tree and folder listings.
Uses the proper path conversion functions instead of raw TrimSuffix.
2026-06-06 15:24:55 -05:00
jmiller 48c354fbb4 Merge pull request 'fix(wiki): preserve slashes in page titles' (#535) from feat/79-wiki-folders into dev
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 1s
2026-06-06 20:16:52 +00:00
Jonathan Miller 41c42b968e fix(wiki): preserve slashes in page titles for folder creation
Generic: Repo Health / Scripts governance (push) Blocked by required conditions
Generic: Repo Health / Repository health (push) Blocked by required conditions
Generic: Repo Health / Report Issues (push) Blocked by required conditions
Generic: Repo Health / Site Health (push) Has been skipped
Generic: Repo Health / Access control (push) Successful in 2s
Universal: PR Check / Build RC Package (pull_request) Blocked by required conditions
Universal: PR Check / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Scripts governance (pull_request) Blocked by required conditions
Generic: Repo Health / Repository health (pull_request) Blocked by required conditions
Generic: Repo Health / Report Issues (pull_request) Blocked by required conditions
Generic: Repo Health / Site Health (pull_request) Has been skipped
Branch Policy Check / Verify merge target (pull_request) Successful in 1s
Universal: PR Check / Branch Policy (pull_request) Successful in 2s
Generic: Repo Health / Access control (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 2s
Universal: PR Check / Validate PR (pull_request) Failing after 6s
Branch Cleanup / Delete merged branch (pull_request) Successful in 2s
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || 'development' }}) (pull_request) Successful in 1m0s
UserTitleToWebPath now splits on / and sanitizes each segment
independently, preserving the directory structure. This allows
creating pages like "features/Custom-Fields" as actual nested files.
2026-06-06 15:15:57 -05:00
41 changed files with 1860 additions and 92 deletions
+50 -1
View File
@@ -445,9 +445,10 @@ server.tool(
assignees: z.array(z.string()).optional().describe('Usernames to assign'),
status_id: z.number().optional().describe('Custom status definition ID'),
priority_id: z.number().optional().describe('Custom priority definition ID'),
type_id: z.number().optional().describe('Custom type definition ID'),
...ConnectionParam,
},
async ({ owner, repo, title, body: issueBody, labels, milestone, assignees, status_id, priority_id, connection }) => {
async ({ owner, repo, title, body: issueBody, labels, milestone, assignees, status_id, priority_id, type_id, connection }) => {
const c = clientFor(connection);
// Search for existing issue with same title to prevent duplicates
@@ -483,6 +484,7 @@ server.tool(
if (issueData?.id) {
if (status_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-status`, { status_id });
if (priority_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-priority`, { priority_id });
if (type_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${issueData.id}/custom-type`, { type_id });
}
const out = formatResponse(res);
out.content[0].text = `Updated existing issue #${existing.number} (duplicate prevented)\n${out.content[0].text}`;
@@ -501,6 +503,7 @@ server.tool(
if (newIssue?.id) {
if (status_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-status`, { status_id });
if (priority_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-priority`, { priority_id });
if (type_id !== undefined) await c.post(`/repos/${owner}/${repo}/issues/${newIssue.id}/custom-type`, { type_id });
}
return formatResponse(res);
},
@@ -2006,6 +2009,52 @@ server.tool(
},
);
// ── Issue Types (org-level) ──────────────────────────────────────────────
server.tool(
'gitea_org_issue_types_list',
'List custom issue type definitions for an organization',
{
org: z.string().describe('Organization name'),
...ConnectionParam,
},
async ({ org, connection }) => {
const c = clientFor(connection);
return formatResponse(await c.get(`/orgs/${org}/issue-types`));
},
);
server.tool(
'gitea_issue_set_type',
'Set custom type on an issue',
{
owner: z.string().describe('Repository owner'),
repo: z.string().describe('Repository name'),
issue_id: z.number().describe('Internal issue ID'),
type_id: z.number().describe('Type definition ID (0 to clear)'),
...ConnectionParam,
},
async ({ owner, repo, issue_id, type_id, connection }) => {
const c = clientFor(connection);
return formatResponse(await c.post(`/repos/${owner}/${repo}/issues/${issue_id}/custom-type`, { type_id }));
},
);
// ── Security ────────────────────────────────────────────────────────────
server.tool(
'gitea_security_alerts',
'List security alerts for a repository',
{
...OwnerRepo,
...ConnectionParam,
},
async ({ owner, repo, connection }) => {
const c = clientFor(connection);
return formatResponse(await c.get(`/repos/${owner}/${repo}/security/alerts`));
},
);
// ── Start Server ────────────────────────────────────────────────────────
async function main(): Promise<void> {
+51
View File
@@ -0,0 +1,51 @@
name: Publish MCP to npm
on:
push:
branches: [main]
paths:
- '.mokogitea/mcp/**'
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
registry-url: 'https://registry.npmjs.org'
- name: Install and build
working-directory: .mokogitea/mcp
run: |
npm ci
npx tsc
- name: Check version change
id: version
working-directory: .mokogitea/mcp
run: |
LOCAL_VERSION=$(node -p "require('./package.json').version")
NPM_VERSION=$(npm view @mokoconsulting/mokogitea-mcp version 2>/dev/null || echo "0.0.0")
if [ "$LOCAL_VERSION" != "$NPM_VERSION" ]; then
echo "changed=true" >> $GITHUB_OUTPUT
echo "Version changed: $NPM_VERSION -> $LOCAL_VERSION"
else
echo "changed=false" >> $GITHUB_OUTPUT
echo "Version unchanged: $LOCAL_VERSION"
fi
- name: Publish to npm
if: steps.version.outputs.changed == 'true'
working-directory: .mokogitea/mcp
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Publish to Gitea registry
if: steps.version.outputs.changed == 'true'
working-directory: .mokogitea/mcp
run: |
npm publish --registry ${{ github.server_url }}/api/packages/${{ github.repository_owner }}/npm/ \
--//$(echo "${{ github.server_url }}" | sed 's|https://||')/api/packages/${{ github.repository_owner }}/npm/:_authToken=${{ secrets.GITEA_TOKEN }}
+40
View File
@@ -3,6 +3,46 @@
All notable changes to MokoGitea are documented here. Versions follow the format
`v{upstream}-moko.{major}.{minor}` (e.g. `v1.26.1-moko.06.03`).
## [v1.26.1-moko.06.10] - 2026-06-06
* FEATURES
* feat(issues): first-class Type field with 12 auto-seeded defaults (Bug, Feature, Enhancement, Task, Documentation, Security, Roadmap, Client, Dolibarr, Infrastructure, Joomla, WaaS)
* feat(issues): first-class Status field with 13 auto-seeded defaults including 7 Pending states
* feat(issues): first-class Priority field with 4 auto-seeded defaults (Critical, High, Medium, Low)
* feat(issues): Type/Status/Priority colored badges in issue list view
* feat(issues): status dropdown replaces close/reopen button in comment form
* feat(security): built-in security scanning platform with secret scanner (15 patterns)
* feat(security): Security tab in repo navigation with alerts, scan controls
* feat(wiki): hierarchical folder navigation with sidebar tree and breadcrumbs
* feat(ui): well-known file tabs (README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG)
* feat(settings): repo manifest settings with REST API and auto-sync on push
* feat(mcp): public MCP server published to npm (@mokoconsulting/mokogitea-mcp)
* feat(mcp): SSE transport, env var config, Docker support, 120+ tools
* feat(mcp): issue dedup on create, type_id/status_id/priority_id params
* MIGRATIONS
* All org labels migrated to first-class Type/Status/Priority fields and deleted
* Type custom field (id=9) migrated to type_id and deleted
* Status custom field (id=1) deleted (replaced by first-class field)
* Priority labels migrated to priority_id
* Pending labels migrated to status definitions
* Scope labels migrated to type definitions
* Manifests populated for all 61 repos via API
* FIXES
* fix(ui): dashboard issue count badges use label spans instead of strong tags
* fix(wiki): directory check before raw redirect for folder navigation
* fix(wiki): proper display names in sidebar tree (strip dash markers)
* fix: replace non-ASCII em dashes with hyphens for hook compatibility
* fix: hookify __init__.py for stop hook JSON validation
* INFRASTRUCTURE
* npm: @mokoconsulting/mokogitea-mcp@1.1.0 and @mokoconsulting/mokowaas-mcp@1.0.0
* MCP servers consolidated under moko-platform/mcp/servers/
* Remote MCP repos renamed to hyphens
* Wiki restructured into features/, api/, operations/ folders
* Swagger API docs enabled at /api/swagger
## [v1.26.1-moko.06.04] - 2026-06-06
* FEATURES
+2
View File
@@ -80,6 +80,8 @@ type Issue struct {
Status *IssueStatusDef `xorm:"-"`
PriorityID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'priority_id'"`
PriorityDef *IssuePriorityDef `xorm:"-"`
TypeID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'type_id'"`
TypeDef *IssueTypeDef `xorm:"-"`
IsRead bool `xorm:"-"`
IsPull bool `xorm:"INDEX"` // Indicates whether is a pull request or not.
PullRequest *PullRequest `xorm:"-"`
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package issues
import (
"context"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
)
func init() {
db.RegisterModel(new(IssueTypeDef))
}
// IssueTypeDef defines a custom issue type at the org level.
type IssueTypeDef struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'org_id'"`
Name string `xorm:"NOT NULL"`
Color string `xorm:"VARCHAR(7)"`
Description string `xorm:"TEXT"`
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
IsDefault bool `xorm:"NOT NULL DEFAULT false 'is_default'"`
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (IssueTypeDef) TableName() string {
return "issue_type_def"
}
// GetIssueTypeDefsByOrg returns active type definitions for an org.
// Auto-seeds defaults if none exist.
func GetIssueTypeDefsByOrg(ctx context.Context, orgID int64) ([]*IssueTypeDef, error) {
defs := make([]*IssueTypeDef, 0, 10)
if err := db.GetEngine(ctx).
Where("org_id = ? AND is_active = ?", orgID, true).
OrderBy("sort_order ASC, id ASC").
Find(&defs); err != nil {
return nil, err
}
if len(defs) == 0 && orgID > 0 {
if err := seedDefaultIssueTypes(ctx, orgID); err != nil {
return defs, nil
}
return GetIssueTypeDefsByOrg(ctx, orgID)
}
return defs, nil
}
// GetAllIssueTypeDefsByOrg returns all type definitions (including inactive).
func GetAllIssueTypeDefsByOrg(ctx context.Context, orgID int64) ([]*IssueTypeDef, error) {
defs := make([]*IssueTypeDef, 0, 10)
return defs, db.GetEngine(ctx).
Where("org_id = ?", orgID).
OrderBy("sort_order ASC, id ASC").
Find(&defs)
}
// GetIssueTypeDefByID returns a single type definition.
func GetIssueTypeDefByID(ctx context.Context, id int64) (*IssueTypeDef, error) {
def := new(IssueTypeDef)
has, err := db.GetEngine(ctx).ID(id).Get(def)
if err != nil {
return nil, err
}
if !has {
return nil, db.ErrNotExist{Resource: "IssueTypeDef", ID: id}
}
return def, nil
}
func CreateIssueTypeDef(ctx context.Context, def *IssueTypeDef) error {
_, err := db.GetEngine(ctx).Insert(def)
return err
}
func UpdateIssueTypeDef(ctx context.Context, def *IssueTypeDef) error {
_, err := db.GetEngine(ctx).ID(def.ID).AllCols().Update(def)
return err
}
func DeleteIssueTypeDef(ctx context.Context, id int64) error {
if _, err := db.GetEngine(ctx).Exec("UPDATE issue SET type_id = 0 WHERE type_id = ?", id); err != nil {
return err
}
_, err := db.GetEngine(ctx).ID(id).Delete(new(IssueTypeDef))
return err
}
func SetIssueTypeID(ctx context.Context, issueID, typeID int64) error {
_, err := db.GetEngine(ctx).Exec("UPDATE issue SET type_id = ? WHERE id = ?", typeID, issueID)
return err
}
func seedDefaultIssueTypes(ctx context.Context, orgID int64) error {
defaults := []*IssueTypeDef{
{OrgID: orgID, Name: "Bug", Color: "#dc2626", SortOrder: 1, IsActive: true},
{OrgID: orgID, Name: "Feature", Color: "#2563eb", SortOrder: 2, IsDefault: true, IsActive: true},
{OrgID: orgID, Name: "Enhancement", Color: "#16a34a", SortOrder: 3, IsActive: true},
{OrgID: orgID, Name: "Task", Color: "#6b7280", SortOrder: 4, IsActive: true},
{OrgID: orgID, Name: "Documentation", Color: "#8b5cf6", SortOrder: 5, IsActive: true},
{OrgID: orgID, Name: "Security", Color: "#e11d48", SortOrder: 6, IsActive: true},
}
for _, d := range defaults {
if _, err := db.GetEngine(ctx).Insert(d); err != nil {
return err
}
}
return nil
}
+2
View File
@@ -426,6 +426,8 @@ func prepareMigrationTasks() []*migration {
newMigration(346, "Add issue status definitions table", v1_27.AddIssueStatusDefTable),
newMigration(347, "Add repo manifest table", v1_27.AddRepoManifestTable),
newMigration(348, "Add issue priority definitions table", v1_27.AddIssuePriorityDefTable),
newMigration(349, "Add security scanning tables", v1_27.AddSecurityScanningTables),
newMigration(350, "Add issue type definitions table", v1_27.AddIssueTypeDefTable),
}
return preparedMigrations
}
+49
View File
@@ -0,0 +1,49 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import (
"xorm.io/xorm"
)
// AddSecurityScanningTables creates security_alert and security_scanner_config tables.
func AddSecurityScanningTables(x *xorm.Engine) error {
type SecurityAlert struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL 'repo_id'"`
Scanner string `xorm:"VARCHAR(20) NOT NULL 'scanner'"`
Severity string `xorm:"VARCHAR(10) NOT NULL 'severity'"`
Status string `xorm:"VARCHAR(10) NOT NULL DEFAULT 'active' 'status'"`
RuleID string `xorm:"VARCHAR(100) NOT NULL 'rule_id'"`
Title string `xorm:"TEXT NOT NULL 'title'"`
Description string `xorm:"TEXT 'description'"`
FilePath string `xorm:"TEXT 'file_path'"`
LineNumber int `xorm:"'line_number'"`
CommitSHA string `xorm:"VARCHAR(64) 'commit_sha'"`
Fingerprint string `xorm:"VARCHAR(64) INDEX 'fingerprint'"`
Metadata string `xorm:"TEXT 'metadata'"`
ResolvedBy int64 `xorm:"'resolved_by'"`
CreatedUnix int64 `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix int64 `xorm:"UPDATED 'updated_unix'"`
}
if err := x.Sync(new(SecurityAlert)); err != nil {
return err
}
type SecurityScannerConfig struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
Enabled bool `xorm:"NOT NULL DEFAULT true 'enabled'"`
BlockOnPush bool `xorm:"NOT NULL DEFAULT false 'block_on_push'"`
SecretScanner bool `xorm:"NOT NULL DEFAULT true 'secret_scanner'"`
DependScanner bool `xorm:"NOT NULL DEFAULT true 'depend_scanner'"`
CodeScanner bool `xorm:"NOT NULL DEFAULT false 'code_scanner'"`
ConfigScanner bool `xorm:"NOT NULL DEFAULT false 'config_scanner'"`
LicenseScanner bool `xorm:"NOT NULL DEFAULT false 'license_scanner'"`
CustomPatterns string `xorm:"TEXT 'custom_patterns'"`
CreatedUnix int64 `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix int64 `xorm:"UPDATED 'updated_unix'"`
}
return x.Sync(new(SecurityScannerConfig))
}
+29
View File
@@ -0,0 +1,29 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package v1_27
import "xorm.io/xorm"
// AddIssueTypeDefTable creates the issue_type_def table and adds type_id to issues.
func AddIssueTypeDefTable(x *xorm.Engine) error {
type IssueTypeDef struct {
ID int64 `xorm:"pk autoincr"`
OrgID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'org_id'"`
Name string `xorm:"NOT NULL"`
Color string `xorm:"VARCHAR(7)"`
Description string `xorm:"TEXT"`
SortOrder int `xorm:"NOT NULL DEFAULT 0 'sort_order'"`
IsDefault bool `xorm:"NOT NULL DEFAULT false 'is_default'"`
IsActive bool `xorm:"NOT NULL DEFAULT true 'is_active'"`
CreatedUnix int64 `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix int64 `xorm:"UPDATED 'updated_unix'"`
}
if err := x.Sync(new(IssueTypeDef)); err != nil {
return err
}
type Issue struct {
TypeID int64 `xorm:"INDEX NOT NULL DEFAULT 0 'type_id'"`
}
return x.Sync(new(Issue))
}
+219
View File
@@ -0,0 +1,219 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package security
import (
"context"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
)
func init() {
db.RegisterModel(new(SecurityAlert))
db.RegisterModel(new(SecurityScannerConfig))
}
// AlertSeverity represents the severity level of a security finding.
type AlertSeverity string
const (
SeverityCritical AlertSeverity = "critical"
SeverityHigh AlertSeverity = "high"
SeverityMedium AlertSeverity = "medium"
SeverityLow AlertSeverity = "low"
SeverityInfo AlertSeverity = "info"
)
// AlertStatus represents the lifecycle state of an alert.
type AlertStatus string
const (
AlertStatusActive AlertStatus = "active"
AlertStatusResolved AlertStatus = "resolved"
AlertStatusDismissed AlertStatus = "dismissed"
)
// ScannerType identifies which scanner produced a finding.
type ScannerType string
const (
ScannerSecret ScannerType = "secret"
ScannerDependency ScannerType = "dependency"
ScannerCode ScannerType = "code"
ScannerConfig ScannerType = "config"
ScannerLicense ScannerType = "license"
)
// SecurityAlert stores a single security finding for a repository.
type SecurityAlert struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"INDEX NOT NULL 'repo_id'"`
Scanner ScannerType `xorm:"VARCHAR(20) NOT NULL 'scanner'"`
Severity AlertSeverity `xorm:"VARCHAR(10) NOT NULL 'severity'"`
Status AlertStatus `xorm:"VARCHAR(10) NOT NULL DEFAULT 'active' 'status'"`
RuleID string `xorm:"VARCHAR(100) NOT NULL 'rule_id'"` // e.g. "aws-access-key", "cve-2024-1234"
Title string `xorm:"TEXT NOT NULL 'title'"`
Description string `xorm:"TEXT 'description'"`
FilePath string `xorm:"TEXT 'file_path'"`
LineNumber int `xorm:"'line_number'"`
CommitSHA string `xorm:"VARCHAR(64) 'commit_sha'"`
Fingerprint string `xorm:"VARCHAR(64) INDEX 'fingerprint'"` // dedup key: hash of rule+file+content
Metadata string `xorm:"TEXT 'metadata'"` // JSON extra data
ResolvedBy int64 `xorm:"'resolved_by'"`
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (SecurityAlert) TableName() string {
return "security_alert"
}
// SecurityScannerConfig stores per-repo scanner settings.
type SecurityScannerConfig struct {
ID int64 `xorm:"pk autoincr"`
RepoID int64 `xorm:"UNIQUE INDEX NOT NULL 'repo_id'"`
Enabled bool `xorm:"NOT NULL DEFAULT true 'enabled'"`
BlockOnPush bool `xorm:"NOT NULL DEFAULT false 'block_on_push'"` // reject push if secrets found
SecretScanner bool `xorm:"NOT NULL DEFAULT true 'secret_scanner'"`
DependScanner bool `xorm:"NOT NULL DEFAULT true 'depend_scanner'"`
CodeScanner bool `xorm:"NOT NULL DEFAULT false 'code_scanner'"`
ConfigScanner bool `xorm:"NOT NULL DEFAULT false 'config_scanner'"`
LicenseScanner bool `xorm:"NOT NULL DEFAULT false 'license_scanner'"`
CustomPatterns string `xorm:"TEXT 'custom_patterns'"` // JSON array of custom regex patterns
CreatedUnix timeutil.TimeStamp `xorm:"INDEX CREATED 'created_unix'"`
UpdatedUnix timeutil.TimeStamp `xorm:"UPDATED 'updated_unix'"`
}
func (SecurityScannerConfig) TableName() string {
return "security_scanner_config"
}
// ──────────────────────────────────────────────────────────────────────
// Alert queries
// ──────────────────────────────────────────────────────────────────────
// GetActiveAlerts returns all active alerts for a repo.
func GetActiveAlerts(ctx context.Context, repoID int64) ([]*SecurityAlert, error) {
alerts := make([]*SecurityAlert, 0, 20)
return alerts, db.GetEngine(ctx).
Where("repo_id = ? AND status = ?", repoID, AlertStatusActive).
OrderBy("severity ASC, created_unix DESC").
Find(&alerts)
}
// GetAllAlerts returns all alerts for a repo (including resolved/dismissed).
func GetAllAlerts(ctx context.Context, repoID int64) ([]*SecurityAlert, error) {
alerts := make([]*SecurityAlert, 0, 50)
return alerts, db.GetEngine(ctx).
Where("repo_id = ?", repoID).
OrderBy("status ASC, severity ASC, created_unix DESC").
Find(&alerts)
}
// GetAlertByID returns a single alert.
func GetAlertByID(ctx context.Context, id int64) (*SecurityAlert, error) {
alert := new(SecurityAlert)
has, err := db.GetEngine(ctx).ID(id).Get(alert)
if err != nil {
return nil, err
}
if !has {
return nil, db.ErrNotExist{Resource: "SecurityAlert", ID: id}
}
return alert, nil
}
// GetAlertCountsByRepo returns count of active alerts grouped by severity.
func GetAlertCountsByRepo(ctx context.Context, repoID int64) (map[AlertSeverity]int64, error) {
type result struct {
Severity AlertSeverity `xorm:"severity"`
Count int64 `xorm:"count"`
}
var results []result
err := db.GetEngine(ctx).
Table("security_alert").
Select("severity, COUNT(*) as count").
Where("repo_id = ? AND status = ?", repoID, AlertStatusActive).
GroupBy("severity").
Find(&results)
if err != nil {
return nil, err
}
counts := make(map[AlertSeverity]int64)
for _, r := range results {
counts[r.Severity] = r.Count
}
return counts, nil
}
// CreateOrUpdateAlert creates a new alert or updates if fingerprint exists.
func CreateOrUpdateAlert(ctx context.Context, alert *SecurityAlert) error {
if alert.Fingerprint != "" {
existing := new(SecurityAlert)
has, err := db.GetEngine(ctx).
Where("repo_id = ? AND fingerprint = ?", alert.RepoID, alert.Fingerprint).
Get(existing)
if err != nil {
return err
}
if has {
// Update existing - refresh commit SHA and keep active
existing.CommitSHA = alert.CommitSHA
existing.LineNumber = alert.LineNumber
existing.Status = AlertStatusActive
_, err = db.GetEngine(ctx).ID(existing.ID).
Cols("commit_sha", "line_number", "status").Update(existing)
return err
}
}
_, err := db.GetEngine(ctx).Insert(alert)
return err
}
// UpdateAlertStatus changes the status of an alert.
func UpdateAlertStatus(ctx context.Context, id int64, status AlertStatus, resolvedBy int64) error {
_, err := db.GetEngine(ctx).ID(id).
Cols("status", "resolved_by").
Update(&SecurityAlert{Status: status, ResolvedBy: resolvedBy})
return err
}
// ──────────────────────────────────────────────────────────────────────
// Scanner config queries
// ──────────────────────────────────────────────────────────────────────
// GetScannerConfig returns the scanner config for a repo, or defaults.
func GetScannerConfig(ctx context.Context, repoID int64) (*SecurityScannerConfig, error) {
cfg := new(SecurityScannerConfig)
has, err := db.GetEngine(ctx).Where("repo_id = ?", repoID).Get(cfg)
if err != nil {
return nil, err
}
if !has {
return &SecurityScannerConfig{
RepoID: repoID,
Enabled: true,
SecretScanner: true,
DependScanner: true,
}, nil
}
return cfg, nil
}
// SaveScannerConfig creates or updates scanner config.
func SaveScannerConfig(ctx context.Context, cfg *SecurityScannerConfig) error {
existing := new(SecurityScannerConfig)
has, err := db.GetEngine(ctx).Where("repo_id = ?", cfg.RepoID).Get(existing)
if err != nil {
return err
}
if has {
cfg.ID = existing.ID
_, err = db.GetEngine(ctx).ID(cfg.ID).AllCols().Update(cfg)
return err
}
_, err = db.GetEngine(ctx).Insert(cfg)
return err
}
+36
View File
@@ -1584,6 +1584,7 @@
"repo.issues.save": "Save",
"repo.issues.status": "Status",
"repo.issues.priority": "Priority",
"repo.issues.type": "Type",
"repo.issues.label_title": "Name",
"repo.issues.label_description": "Description",
"repo.issues.label_color": "Color",
@@ -1969,6 +1970,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.",
@@ -2750,6 +2752,28 @@
"repo.settings.manifest_entry_point": "Entry Point",
"repo.settings.manifest_save": "Save Manifest",
"repo.settings.manifest_saved": "Manifest settings saved.",
"repo.settings.security": "Security",
"repo.settings.security_desc": "Security scanning detects secrets, vulnerabilities, and code issues across the repository.",
"repo.settings.security_scanners": "Scanners",
"repo.settings.security_enabled": "Enable security scanning",
"repo.settings.security_secret_scanner": "Secret Scanner - API keys, tokens, passwords, private keys",
"repo.settings.security_depend_scanner": "Dependency Scanner - CVEs in dependencies (coming soon)",
"repo.settings.security_code_scanner": "Code Scanner - SQL injection, XSS, command injection (coming soon)",
"repo.settings.security_config_scanner": "Config Scanner - Insecure settings, debug modes (coming soon)",
"repo.settings.security_license_scanner": "License Scanner - License compliance (coming soon)",
"repo.settings.security_block_on_push": "Block pushes with critical findings",
"repo.settings.security_block_on_push_help": "Reject pushes to the default branch if critical secrets are detected.",
"repo.settings.security_save": "Save Settings",
"repo.settings.security_saved": "Security settings saved.",
"repo.settings.security_alerts": "Security Alerts",
"repo.settings.security_scan_now": "Scan Now",
"repo.settings.security_scan_complete": "Security scan complete.",
"repo.settings.security_severity": "Severity",
"repo.settings.security_scanner_type": "Scanner",
"repo.settings.security_finding": "Finding",
"repo.settings.security_file": "File",
"repo.settings.security_status": "Status",
"repo.settings.security_no_alerts": "No security alerts found. Run a scan or push to the default branch to check.",
"repo.settings.metadata": "Metadata",
"repo.settings.metadata_saved": "Repository metadata saved.",
"repo.settings.metadata_empty": "No metadata fields defined. Org admins can add fields in Organization Settings > Custom Fields.",
@@ -2968,6 +2992,18 @@
"org.settings.issue_priority_created": "Issue priority created.",
"org.settings.issue_priority_updated": "Issue priority updated.",
"org.settings.issue_priority_deleted": "Issue priority deleted.",
"org.settings.issue_types": "Issue Types",
"org.settings.issue_types_desc": "Define issue types for all repositories in this organization.",
"org.settings.issue_types_empty": "No custom issue types defined yet.",
"org.settings.issue_type_add": "Add Type",
"org.settings.issue_type_name": "Type Name",
"org.settings.issue_type_color": "Color",
"org.settings.issue_type_description": "Description",
"org.settings.issue_type_default": "Default",
"org.settings.issue_type_sort_order": "Sort Order",
"org.settings.issue_type_created": "Issue type created.",
"org.settings.issue_type_updated": "Issue type updated.",
"org.settings.issue_type_deleted": "Issue type deleted.",
"org.settings.update_streams": "Update Server",
"org.settings.licensing": "Update Server",
"org.settings.licensing_desc": "Manage update feeds and optional license key gating across all repositories in this organization.",
+98
View File
@@ -0,0 +1,98 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package org
import (
"net/http"
"strconv"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/templates"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
const tplOrgIssueTypes templates.TplName = "org/settings/issue_types"
func SettingsIssueTypes(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("org.settings.issue_types")
ctx.Data["PageIsOrgSettings"] = true
ctx.Data["PageIsSettingsIssueTypes"] = true
defs, err := issues_model.GetAllIssueTypeDefsByOrg(ctx, ctx.Org.Organization.ID)
if err != nil {
ctx.ServerError("GetAllIssueTypeDefsByOrg", err)
return
}
ctx.Data["IssueTypes"] = defs
ctx.HTML(http.StatusOK, tplOrgIssueTypes)
}
func SettingsIssueTypesCreatePost(ctx *context.Context) {
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
def := &issues_model.IssueTypeDef{
OrgID: ctx.Org.Organization.ID,
Name: ctx.FormString("name"),
Color: ctx.FormString("color"),
Description: ctx.FormString("description"),
SortOrder: sortOrder,
IsDefault: ctx.FormString("is_default") == "on",
IsActive: true,
}
if def.Name == "" {
ctx.Flash.Error("Type name is required")
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-types")
return
}
if err := issues_model.CreateIssueTypeDef(ctx, def); err != nil {
ctx.ServerError("CreateIssueTypeDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_type_created"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-types")
}
func SettingsIssueTypesEditPost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
def, err := issues_model.GetIssueTypeDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetIssueTypeDefByID", err)
return
}
if def.OrgID != ctx.Org.Organization.ID {
ctx.NotFound(nil)
return
}
def.Name = ctx.FormString("name")
def.Color = ctx.FormString("color")
def.Description = ctx.FormString("description")
def.IsDefault = ctx.FormString("is_default") == "on"
def.IsActive = ctx.FormString("is_active") == "on"
sortOrder, _ := strconv.Atoi(ctx.FormString("sort_order"))
def.SortOrder = sortOrder
if err := issues_model.UpdateIssueTypeDef(ctx, def); err != nil {
ctx.ServerError("UpdateIssueTypeDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_type_updated"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-types")
}
func SettingsIssueTypesDeletePost(ctx *context.Context) {
id := ctx.PathParamInt64("id")
def, err := issues_model.GetIssueTypeDefByID(ctx, id)
if err != nil {
ctx.ServerError("GetIssueTypeDefByID", err)
return
}
if def.OrgID != ctx.Org.Organization.ID {
ctx.NotFound(nil)
return
}
if err := issues_model.DeleteIssueTypeDef(ctx, id); err != nil {
ctx.ServerError("DeleteIssueTypeDef", err)
return
}
ctx.Flash.Success(ctx.Tr("org.settings.issue_type_deleted"))
ctx.Redirect(ctx.Org.OrgLink + "/settings/issue-types")
}
+43
View File
@@ -0,0 +1,43 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package repo
import (
"fmt"
"net/http"
issues_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/issues"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/context"
)
// UpdateIssueCustomType handles POST to set a custom type on an issue.
func UpdateIssueCustomType(ctx *context.Context) {
issueID := ctx.PathParamInt64("id")
typeID := ctx.FormInt64("type_id")
issue, err := issues_model.GetIssueByID(ctx, issueID)
if err != nil {
ctx.ServerError("GetIssueByID", err)
return
}
if typeID > 0 {
typeDef, err := issues_model.GetIssueTypeDefByID(ctx, typeID)
if err != nil {
ctx.ServerError("GetIssueTypeDefByID", err)
return
}
if typeDef.OrgID != ctx.Repo.Repository.OwnerID {
ctx.NotFound(nil)
return
}
}
if err := issues_model.SetIssueTypeID(ctx, issueID, typeID); err != nil {
ctx.ServerError("SetIssueTypeID", err)
return
}
ctx.Redirect(fmt.Sprintf("%s/issues/%d", ctx.Repo.RepoLink, issue.Index), http.StatusSeeOther)
}
+8
View File
@@ -536,6 +536,14 @@ func prepareIssueFilterAndList(ctx *context.Context, milestoneID int64, projectI
}
ctx.Data["CustomFieldDefs"] = customFieldDefs
ctx.Data["CustomFieldFilters"] = customFieldFilters
// Load first-class field definitions for issue list badges
issueStatusDefs, _ := issues_model.GetIssueStatusDefsByOrg(ctx, repo.OwnerID)
ctx.Data["IssueStatusDefs"] = issueStatusDefs
issuePriorityDefs, _ := issues_model.GetIssuePriorityDefsByOrg(ctx, repo.OwnerID)
ctx.Data["IssuePriorityDefs"] = issuePriorityDefs
issueTypeDefs, _ := issues_model.GetIssueTypeDefsByOrg(ctx, repo.OwnerID)
ctx.Data["IssueTypeDefs"] = issueTypeDefs
// Build a query string fragment for cf_ params so they survive pagination/sort changes.
cfQuery := make(url.Values)
for fieldID, value := range customFieldFilters {
+7
View File
@@ -379,6 +379,13 @@ func ViewIssue(ctx *context.Context) {
}
ctx.Data["IssuePriorityDefs"] = issuePriorityDefs
// Load custom issue type definitions for the sidebar.
issueTypeDefs, itErr := issues_model.GetIssueTypeDefsByOrg(ctx, ctx.Repo.Repository.OwnerID)
if itErr != nil {
log.Error("ViewIssue: GetIssueTypeDefsByOrg: %v", itErr)
}
ctx.Data["IssueTypeDefs"] = issueTypeDefs
upload.AddUploadContext(ctx, "comment")
if err := issue.LoadAttributes(ctx); err != nil {
+88
View File
@@ -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")
}
+1 -1
View File
@@ -88,7 +88,7 @@ func ManifestSettingsPost(ctx *context.Context) {
RepoID: ctx.Repo.Repository.ID,
Name: ctx.FormString("name"),
Org: ctx.FormString("org"),
Description: ctx.FormString("description"),
Description: ctx.Repo.Repository.Description,
Version: ctx.FormString("version"),
LicenseSPDX: ctx.FormString("license_spdx"),
LicenseName: ctx.FormString("license_name"),
+111
View File
@@ -0,0 +1,111 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package setting
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 tplSettingsSecurity templates.TplName = "repo/settings/security"
// SecuritySettings displays the repo security scanning settings and alerts.
func SecuritySettings(ctx *context.Context) {
ctx.Data["Title"] = ctx.Tr("repo.settings.security")
ctx.Data["PageIsSettingsSecurity"] = 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, tplSettingsSecurity)
}
// SecuritySettingsPost saves security scanner configuration.
func SecuritySettingsPost(ctx *context.Context) {
cfg := &security_model.SecurityScannerConfig{
RepoID: ctx.Repo.Repository.ID,
Enabled: ctx.FormString("enabled") == "on",
BlockOnPush: ctx.FormString("block_on_push") == "on",
SecretScanner: ctx.FormString("secret_scanner") == "on",
DependScanner: ctx.FormString("depend_scanner") == "on",
CodeScanner: ctx.FormString("code_scanner") == "on",
ConfigScanner: ctx.FormString("config_scanner") == "on",
LicenseScanner: ctx.FormString("license_scanner") == "on",
}
if err := security_model.SaveScannerConfig(ctx, cfg); err != nil {
ctx.ServerError("SaveScannerConfig", err)
return
}
ctx.Flash.Success(ctx.Tr("repo.settings.security_saved"))
ctx.Redirect(ctx.Repo.RepoLink + "/settings/security")
}
// SecurityScanNow triggers an immediate scan of the repository.
func SecurityScanNow(ctx *context.Context) {
commit := ctx.Repo.Commit
if commit == nil {
ctx.Flash.Error("No commits found in repository")
ctx.Redirect(ctx.Repo.RepoLink + "/settings/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 + "/settings/security")
}
// SecurityAlertUpdate changes the status of a security alert.
func SecurityAlertUpdate(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 + "/settings/security")
}
+62 -41
View File
@@ -254,38 +254,39 @@ func renderViewPage(ctx *context.Context) (*git.Repository, *git.TreeEntry) {
wikiTree := buildWikiTree(commit)
ctx.Data["WikiTree"] = wikiTree
// Check if path is a directory first (before file lookup)
dirEntry, _ := commit.GetTreeEntryByPath(string(pageName))
if dirEntry != nil && dirEntry.IsDir() {
// Path is a directory - try index files or show folder listing
var entry *git.TreeEntry
foundIndex := false
for _, indexName := range []string{"README", "Home", "index"} {
indexPath := wiki_service.WebPath(string(pageName) + "/" + indexName)
idxEntry, _, idxNoEntry, _ := wikiEntryByName(ctx, commit, indexPath)
if !idxNoEntry && idxEntry != nil {
pageName = indexPath
entry = idxEntry
_, displayName = wiki_service.WebPathToUserTitle(pageName)
ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName)
ctx.Data["Title"] = displayName
foundIndex = true
break
}
}
if !foundIndex {
ctx.Data["IsWikiFolder"] = true
ctx.Data["WikiFolderPath"] = string(pageName)
folderEntries := listWikiFolderEntries(commit, string(pageName))
ctx.Data["WikiFolderEntries"] = folderEntries
return wikiGitRepo, nil
}
_ = entry // will be used below via pageName lookup
}
// lookup filename in wiki - get gitTree entry , real filename
entry, pageFilename, noEntry, isRaw := wikiEntryByName(ctx, commit, pageName)
if noEntry {
// Check if path is a directory - try index files or show folder listing
dirEntry, _ := commit.GetTreeEntryByPath(string(pageName))
if dirEntry != nil && dirEntry.IsDir() {
// Try index files: README.md, Home.md, index.md
for _, indexName := range []string{"README", "Home", "index"} {
indexPath := wiki_service.WebPath(string(pageName) + "/" + indexName)
idxEntry, _, idxNoEntry, _ := wikiEntryByName(ctx, commit, indexPath)
if !idxNoEntry && idxEntry != nil {
// Found index file - render it
pageName = indexPath
entry = idxEntry
_, displayName = wiki_service.WebPathToUserTitle(pageName)
ctx.Data["PageURL"] = wiki_service.WebPathToURLPath(pageName)
ctx.Data["Title"] = displayName
noEntry = false
break
}
}
if noEntry {
// No index file - show folder listing
ctx.Data["IsWikiFolder"] = true
ctx.Data["WikiFolderPath"] = string(pageName)
folderEntries := listWikiFolderEntries(commit, string(pageName))
ctx.Data["WikiFolderEntries"] = folderEntries
return wikiGitRepo, nil
}
} else {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages")
}
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/?action=_pages")
}
if isRaw {
ctx.Redirect(ctx.Repo.RepoLink + "/wiki/raw/" + string(pageName))
@@ -529,6 +530,14 @@ func Wiki(ctx *context.Context) {
if ctx.Written() {
return
}
// Folder listing - no entry but IsWikiFolder flag is set
if ctx.Data["IsWikiFolder"] != nil {
if wikiGitRepo != nil {
defer wikiGitRepo.Close()
}
ctx.HTML(http.StatusOK, tplWikiView)
return
}
if entry == nil {
ctx.Data["Title"] = ctx.Tr("repo.wiki")
ctx.HTML(http.StatusOK, tplWikiStart)
@@ -854,13 +863,17 @@ func buildWikiTree(commit *git.Commit) []*WikiTreeNode {
IsDir: true,
})
} else if strings.HasSuffix(childName, ".md") {
wpName := strings.TrimSuffix(childName, ".md")
if wpName == "_Sidebar" || wpName == "_Footer" {
wpChild, err := wiki_service.GitPathToWebPath(childName)
if err != nil {
continue
}
_, childDisplay := wiki_service.WebPathToUserTitle(wpChild)
if childDisplay == "_Sidebar" || childDisplay == "_Footer" {
continue
}
node.Children = append(node.Children, &WikiTreeNode{
Name: wpName,
SubURL: name + "/" + wpName,
Name: childDisplay,
SubURL: name + "/" + string(wpChild),
IsDir: false,
})
}
@@ -869,13 +882,17 @@ func buildWikiTree(commit *git.Commit) []*WikiTreeNode {
root[name] = node
topLevel = append(topLevel, node)
} else if strings.HasSuffix(name, ".md") {
wpName := strings.TrimSuffix(name, ".md")
if wpName == "_Sidebar" || wpName == "_Footer" {
wpName, err := wiki_service.GitPathToWebPath(name)
if err != nil {
continue
}
_, displayName := wiki_service.WebPathToUserTitle(wpName)
if displayName == "_Sidebar" || displayName == "_Footer" {
continue
}
node := &WikiTreeNode{
Name: wpName,
SubURL: wpName,
Name: displayName,
SubURL: string(wpName),
IsDir: false,
}
topLevel = append(topLevel, node)
@@ -908,13 +925,17 @@ func listWikiFolderEntries(commit *git.Commit, treePath string) []PageMeta {
GitEntryName: name,
})
} else if strings.HasSuffix(name, ".md") {
wpName := strings.TrimSuffix(name, ".md")
if wpName == "_Sidebar" || wpName == "_Footer" {
wpName, err := wiki_service.GitPathToWebPath(name)
if err != nil {
continue
}
_, displayName := wiki_service.WebPathToUserTitle(wpName)
if displayName == "_Sidebar" || displayName == "_Footer" {
continue
}
pages = append(pages, PageMeta{
Name: wpName,
SubURL: treePath + "/" + wpName,
Name: displayName,
SubURL: treePath + "/" + string(wpName),
GitEntryName: name,
})
}
+19
View File
@@ -1079,6 +1079,12 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/{id}/edit", org.SettingsIssuePrioritiesEditPost)
m.Post("/{id}/delete", org.SettingsIssuePrioritiesDeletePost)
})
m.Group("/issue-types", func() {
m.Get("", org.SettingsIssueTypes)
m.Post("", org.SettingsIssueTypesCreatePost)
m.Post("/{id}/edit", org.SettingsIssueTypesEditPost)
m.Post("/{id}/delete", org.SettingsIssueTypesDeletePost)
})
}, ctxDataSet("EnableOAuth2", setting.OAuth2.Enabled, "EnablePackages", setting.Packages.Enabled, "PageIsOrgSettings", true))
}, context.OrgAssignment(context.OrgAssignmentOptions{RequireOwner: true}))
}, reqSignIn)
@@ -1207,6 +1213,11 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Combo("/licensing").Get(repo_setting.LicensingSettings).Post(repo_setting.LicensingSettingsPost)
m.Combo("/manifest").Get(repo_setting.ManifestSettings).Post(repo_setting.ManifestSettingsPost)
m.Combo("/metadata").Get(repo_setting.Metadata).Post(repo_setting.MetadataPost)
m.Group("/security", func() {
m.Combo("").Get(repo_setting.SecuritySettings).Post(repo_setting.SecuritySettingsPost)
m.Post("/scan", repo_setting.SecurityScanNow)
m.Post("/alert/{id}", repo_setting.SecurityAlertUpdate)
})
m.Group("/collaboration", func() {
m.Combo("").Get(repo_setting.Collaboration).Post(repo_setting.CollaborationPost)
@@ -1414,6 +1425,7 @@ func registerWebRoutes(m *web.Router, webAuth *AuthMiddleware) {
m.Post("/{id}/custom-fields/{field_id}", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomField)
m.Post("/{id}/custom-status", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomStatus)
m.Post("/{id}/custom-priority", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomPriority)
m.Post("/{id}/custom-type", reqRepoIssuesOrPullsWriter, repo.UpdateIssueCustomType)
m.Post("/delete", reqRepoAdmin, repo.BatchDeleteIssues)
m.Delete("/unpin/{index}", reqRepoAdmin, repo.IssueUnpin)
m.Post("/move_pin", reqRepoAdmin, repo.IssuePinMove)
@@ -1672,6 +1684,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)
+26
View File
@@ -16,8 +16,11 @@ import (
"syscall"
"time"
auth_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/auth"
user_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/user"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/httplib"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/optional"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/auth/source/oauth2"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/setting"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/structs"
@@ -166,6 +169,18 @@ func (ctx *Context) notFoundInternal(logMsg string, logErr error) {
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
ctx.Data["Title"] = "Page Not Found"
ctx.Data["ErrorMsg"] = "" // FIXME: the template never renders this message, need to fix in the future (and show safe messages to end users)
ctx.Data["CurrentURL"] = ctx.Req.URL.RequestURI()
// Load OAuth2 providers for the login form on error pages
if !ctx.IsSigned {
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
log.Error("NotFound: GetOAuth2Providers: %v", err)
}
ctx.Data["OAuth2Providers"] = oauth2Providers
ctx.Data["EnableSSPI"] = auth_model.IsSSPIEnabled(ctx)
}
ctx.HTML(http.StatusNotFound, "status/404")
}
@@ -187,6 +202,17 @@ func (ctx *Context) Forbidden() {
ctx.Data["IsRepo"] = ctx.Repo.Repository != nil
ctx.Data["Title"] = "Access Denied"
ctx.Data["CurrentURL"] = ctx.Req.URL.RequestURI()
// Load OAuth2 providers for the login form on the 403 page
if !ctx.IsSigned {
oauth2Providers, err := oauth2.GetOAuth2Providers(ctx, optional.Some(true))
if err != nil {
log.Error("Forbidden: GetOAuth2Providers: %v", err)
}
ctx.Data["OAuth2Providers"] = oauth2Providers
ctx.Data["EnableSSPI"] = auth_model.IsSSPIEnabled(ctx)
}
ctx.HTML(http.StatusForbidden, "status/403")
}
+3
View File
@@ -27,6 +27,7 @@ import (
issue_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/issue"
notify_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/notify"
pull_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/pull"
security_service "code.mokoconsulting.tech/MokoConsulting/MokoGitea/services/security"
)
// pushQueue represents a queue to handle update pull request tests
@@ -195,6 +196,8 @@ func pushUpdates(optsList []*repo_module.PushUpdateOptions) error {
}
// Auto-sync .mokogitea/manifest.xml to database on default branch push
SyncManifestFromCommit(ctx, repo, newCommit)
// Run security scanners on default branch push
security_service.ScanOnPush(ctx, repo, newCommit)
} else {
if err := DelDivergenceFromCache(repo.ID, branch); err != nil {
log.Error("DelDivergenceFromCache: %v", err)
+75
View File
@@ -0,0 +1,75 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package security
import (
"context"
repo_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/repo"
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
)
// ScanOnPush runs enabled scanners against a commit pushed to the default branch.
// Called from services/repository/push.go on default branch pushes.
func ScanOnPush(ctx context.Context, repo *repo_model.Repository, commit *git.Commit) {
if commit == nil {
return
}
cfg, err := security_model.GetScannerConfig(ctx, repo.ID)
if err != nil {
log.Error("SecurityScan: GetScannerConfig for %s: %v", repo.FullName(), err)
return
}
if !cfg.Enabled {
return
}
var scanners []Scanner
if cfg.SecretScanner {
scanners = append(scanners, NewSecretScanner())
}
// Future scanners added here:
// if cfg.DependScanner { scanners = append(scanners, NewDependencyScanner()) }
// if cfg.CodeScanner { scanners = append(scanners, NewCodeScanner()) }
if len(scanners) == 0 {
return
}
totalFindings := 0
for _, s := range scanners {
findings, err := s.ScanTree(commit)
if err != nil {
log.Error("SecurityScan: %s scanner for %s: %v", s.Type(), repo.FullName(), err)
continue
}
for _, f := range findings {
alert := &security_model.SecurityAlert{
RepoID: repo.ID,
Scanner: f.Scanner,
Severity: f.Severity,
RuleID: f.RuleID,
Title: f.Title,
Description: f.Description,
FilePath: f.FilePath,
LineNumber: f.LineNumber,
CommitSHA: f.CommitSHA,
Fingerprint: f.Fingerprint,
Metadata: f.Metadata,
}
if err := security_model.CreateOrUpdateAlert(ctx, alert); err != nil {
log.Error("SecurityScan: CreateOrUpdateAlert: %v", err)
}
totalFindings++
}
}
if totalFindings > 0 {
log.Warn("SecurityScan: %d findings in %s", totalFindings, repo.FullName())
}
}
+35
View File
@@ -0,0 +1,35 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package security
import (
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
)
// Finding represents a single security issue found by a scanner.
type Finding struct {
Scanner security_model.ScannerType
Severity security_model.AlertSeverity
RuleID string
Title string
Description string
FilePath string
LineNumber int
CommitSHA string
Fingerprint string // unique identifier for dedup
Metadata string // JSON extra data
}
// Scanner is the interface all security scanner modules implement.
type Scanner interface {
// Type returns the scanner type identifier.
Type() security_model.ScannerType
// ScanCommit scans a single commit and returns findings.
ScanCommit(commit *git.Commit) ([]Finding, error)
// ScanTree scans the full repository tree and returns findings.
ScanTree(commit *git.Commit) ([]Finding, error)
}
+203
View File
@@ -0,0 +1,203 @@
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
// SPDX-License-Identifier: GPL-3.0-or-later
package security
import (
"bufio"
"crypto/sha256"
"fmt"
"io"
"regexp"
"strings"
security_model "code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/security"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/git"
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
)
// SecretRule defines a pattern to match against file contents.
type SecretRule struct {
ID string
Title string
Pattern *regexp.Regexp
Severity security_model.AlertSeverity
Description string
}
// DefaultSecretRules contains the built-in secret detection patterns.
var DefaultSecretRules = []SecretRule{
// AWS
{ID: "aws-access-key", Title: "AWS Access Key ID", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`AKIA[0-9A-Z]{16}`), Description: "AWS access key ID detected"},
{ID: "aws-secret-key", Title: "AWS Secret Access Key", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`(?i)aws_secret_access_key\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}`), Description: "AWS secret access key detected"},
// Generic tokens/keys
{ID: "private-key", Title: "Private Key", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`-----BEGIN (RSA|EC|OPENSSH|DSA|PGP) PRIVATE KEY-----`), Description: "Private key file detected"},
{ID: "generic-api-key", Title: "Generic API Key", Severity: security_model.SeverityHigh,
Pattern: regexp.MustCompile(`(?i)(api[_-]?key|apikey)\s*[=:]\s*['"]?[A-Za-z0-9_\-]{20,}`), Description: "API key assignment detected"},
{ID: "generic-secret", Title: "Generic Secret", Severity: security_model.SeverityHigh,
Pattern: regexp.MustCompile(`(?i)(secret|password|passwd|pwd)\s*[=:]\s*['"][^'"]{8,}['"]`), Description: "Hardcoded secret or password detected"},
{ID: "generic-token", Title: "Generic Token", Severity: security_model.SeverityHigh,
Pattern: regexp.MustCompile(`(?i)(token|auth_token|access_token)\s*[=:]\s*['"]?[A-Za-z0-9_\-.]{20,}`), Description: "Token assignment detected"},
// GitHub/Gitea
{ID: "github-pat", Title: "GitHub Personal Access Token", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`ghp_[A-Za-z0-9]{36}`), Description: "GitHub personal access token detected"},
{ID: "github-oauth", Title: "GitHub OAuth Token", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`gho_[A-Za-z0-9]{36}`), Description: "GitHub OAuth token detected"},
// Stripe
{ID: "stripe-secret", Title: "Stripe Secret Key", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`sk_live_[A-Za-z0-9]{24,}`), Description: "Stripe live secret key detected"},
{ID: "stripe-publishable", Title: "Stripe Publishable Key", Severity: security_model.SeverityLow,
Pattern: regexp.MustCompile(`pk_live_[A-Za-z0-9]{24,}`), Description: "Stripe live publishable key detected (usually safe but flagged)"},
// JWT
{ID: "jwt-token", Title: "JWT Token", Severity: security_model.SeverityMedium,
Pattern: regexp.MustCompile(`eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}`), Description: "JWT token detected"},
// Connection strings
{ID: "connection-string", Title: "Connection String with Password", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`(?i)(mysql|postgres|postgresql|mongodb|redis|amqp|smtp)://[^:]+:[^@]+@[^\s]+`), Description: "Database/service connection string with embedded password"},
// Google
{ID: "google-api-key", Title: "Google API Key", Severity: security_model.SeverityHigh,
Pattern: regexp.MustCompile(`AIza[0-9A-Za-z_-]{35}`), Description: "Google API key detected"},
// Slack
{ID: "slack-webhook", Title: "Slack Webhook URL", Severity: security_model.SeverityMedium,
Pattern: regexp.MustCompile(`https://hooks\.slack\.com/services/T[A-Z0-9]+/B[A-Z0-9]+/[A-Za-z0-9]+`), Description: "Slack webhook URL detected"},
// SendGrid
{ID: "sendgrid-api-key", Title: "SendGrid API Key", Severity: security_model.SeverityHigh,
Pattern: regexp.MustCompile(`SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}`), Description: "SendGrid API key detected"},
// PayPal
{ID: "paypal-client-secret", Title: "PayPal Client Secret", Severity: security_model.SeverityCritical,
Pattern: regexp.MustCompile(`(?i)paypal.*secret\s*[=:]\s*['"]?[A-Za-z0-9_-]{20,}`), Description: "PayPal client secret detected"},
}
// Files to skip during scanning.
var skipExtensions = map[string]bool{
".png": true, ".jpg": true, ".jpeg": true, ".gif": true, ".ico": true,
".svg": true, ".woff": true, ".woff2": true, ".ttf": true, ".eot": true,
".zip": true, ".tar": true, ".gz": true, ".bz2": true, ".7z": true,
".pdf": true, ".doc": true, ".docx": true, ".xls": true, ".xlsx": true,
".exe": true, ".dll": true, ".so": true, ".dylib": true, ".o": true,
".min.js": true, ".min.css": true,
}
var skipPaths = []string{
"vendor/", "node_modules/", ".git/", "dist/", "build/",
"go.sum", "package-lock.json", "composer.lock", "yarn.lock",
}
// SecretScanner implements the Scanner interface for secret detection.
type SecretScanner struct {
Rules []SecretRule
}
// NewSecretScanner creates a scanner with default rules.
func NewSecretScanner() *SecretScanner {
return &SecretScanner{Rules: DefaultSecretRules}
}
func (s *SecretScanner) Type() security_model.ScannerType {
return security_model.ScannerSecret
}
func (s *SecretScanner) ScanCommit(commit *git.Commit) ([]Finding, error) {
// For push-time scanning, we scan the diff of the commit
return s.ScanTree(commit)
}
func (s *SecretScanner) ScanTree(commit *git.Commit) ([]Finding, error) {
if commit == nil {
return nil, nil
}
entries, err := commit.ListEntriesRecursiveFast()
if err != nil {
return nil, fmt.Errorf("ListEntriesRecursiveFast: %w", err)
}
var findings []Finding
for _, entry := range entries {
if !entry.IsRegular() {
continue
}
path := entry.Name()
if shouldSkipFile(path) {
continue
}
// Skip large files (> 1MB)
if entry.Blob().Size() > 1024*1024 {
continue
}
reader, err := entry.Blob().DataAsync()
if err != nil {
log.Trace("SecretScanner: skip %s: %v", path, err)
continue
}
fileFindings := s.scanReader(reader, path, commit.ID.String())
reader.Close()
findings = append(findings, fileFindings...)
}
return findings, nil
}
func (s *SecretScanner) scanReader(r io.Reader, filePath, commitSHA string) []Finding {
var findings []Finding
scanner := bufio.NewScanner(r)
lineNum := 0
for scanner.Scan() {
lineNum++
line := scanner.Text()
for _, rule := range s.Rules {
if rule.Pattern.MatchString(line) {
fingerprint := fmt.Sprintf("%x", sha256.Sum256([]byte(rule.ID+":"+filePath+":"+line)))
findings = append(findings, Finding{
Scanner: security_model.ScannerSecret,
Severity: rule.Severity,
RuleID: rule.ID,
Title: rule.Title,
Description: rule.Description,
FilePath: filePath,
LineNumber: lineNum,
CommitSHA: commitSHA,
Fingerprint: fingerprint[:32],
})
break // one finding per line per file
}
}
}
return findings
}
func shouldSkipFile(path string) bool {
lower := strings.ToLower(path)
for _, skip := range skipPaths {
if strings.HasPrefix(lower, skip) || strings.Contains(lower, "/"+skip) {
return true
}
}
for ext := range skipExtensions {
if strings.HasSuffix(lower, ext) {
return true
}
}
return false
}
+25 -9
View File
@@ -151,14 +151,16 @@ func WebPathFromRequest(s string) WebPath {
var multiHyphenRe = regexp.MustCompile(`-{2,}`)
var nonSlugRe = regexp.MustCompile(`[^a-zA-Z0-9+.\-]`)
var nonSlugReWithSlash = regexp.MustCompile(`[^a-zA-Z0-9+.\-/]`)
// sanitizeWikiTitle converts a user-provided title into a clean, URL-friendly slug.
// Spaces and special characters become hyphens, consecutive hyphens collapse to one.
// Preserves: letters, digits, hyphens, plus signs (+), and dots (.)
// Preserves: letters, digits, hyphens, plus signs (+), dots (.), and slashes (/).
func sanitizeWikiTitle(title string) string {
title = strings.TrimSpace(title)
title = strings.ReplaceAll(title, " ", "-")
title = nonSlugRe.ReplaceAllString(title, "-")
// Preserve slashes as directory separators
title = nonSlugReWithSlash.ReplaceAllString(title, "-")
title = multiHyphenRe.ReplaceAllString(title, "-")
title = strings.NewReplacer("-+-", "-", "+-", "-", "-+", "-").Replace(title) // clean stray plus signs
title = strings.Trim(title, "-+.")
@@ -166,14 +168,28 @@ func sanitizeWikiTitle(title string) string {
}
func UserTitleToWebPath(base, title string) WebPath {
// TODO: no support for subdirectory, because the old wiki code's behavior is always using %2F, instead of subdirectory.
// So we do not add the support for writing slashes in title at the moment.
title = sanitizeWikiTitle(title)
title = util.PathJoinRelX(base, escapeSegToWeb(title, false))
if title == "" || title == "." {
title = "unnamed"
// MokoGitea: support subdirectories - slashes in title create folder structure.
// Split on /, sanitize each segment, rejoin.
parts := strings.Split(title, "/")
sanitized := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p == "" {
continue
}
p = sanitizeWikiTitle(p)
if p != "" {
sanitized = append(sanitized, escapeSegToWeb(p, false))
}
}
return WebPath(title)
result := strings.Join(sanitized, "/")
if base != "" {
result = util.PathJoinRelX(base, result)
}
if result == "" || result == "." {
result = "unnamed"
}
return WebPath(result)
}
// ToWikiPageMetaData converts meta information to a WikiPageMetaData
+81
View File
@@ -0,0 +1,81 @@
{{template "org/settings/layout_head" (dict "ctxData" . "pageClass" "organization settings issue-types")}}
<h4 class="ui top attached header">
{{ctx.Locale.Tr "org.settings.issue_types"}}
</h4>
<div class="ui attached segment">
<p class="text grey">{{ctx.Locale.Tr "org.settings.issue_types_desc"}}</p>
{{if .IssueTypes}}
<table class="ui compact table">
<thead>
<tr>
<th>{{ctx.Locale.Tr "org.settings.issue_type_color"}}</th>
<th>{{ctx.Locale.Tr "org.settings.issue_type_name"}}</th>
<th>{{ctx.Locale.Tr "org.settings.issue_type_default"}}</th>
<th>{{ctx.Locale.Tr "org.settings.issue_type_sort_order"}}</th>
<th></th>
</tr>
</thead>
<tbody>
{{range .IssueTypes}}
<tr {{if not .IsActive}}class="tw-opacity-50"{{end}}>
<td>
{{if .Color}}<span class="tw-inline-block tw-w-4 tw-h-4 tw-rounded" style="background-color: {{.Color}}"></span>{{else}}<span class="text grey">-</span>{{end}}
</td>
<td>
<strong>{{.Name}}</strong>
{{if not .IsActive}}<span class="ui mini grey label">Inactive</span>{{end}}
{{if .Description}}<br><small class="text grey">{{.Description}}</small>{{end}}
</td>
<td>
{{if .IsDefault}}<span class="ui mini blue label">{{ctx.Locale.Tr "org.settings.issue_type_default"}}</span>{{else}}<span class="text grey">-</span>{{end}}
</td>
<td>{{.SortOrder}}</td>
<td class="tw-text-right">
<form method="post" action="{{$.OrgLink}}/settings/issue-types/{{.ID}}/delete" class="tw-inline">
{{$.CsrfTokenHtml}}
<button class="ui tiny red icon button" type="submit" title="{{ctx.Locale.Tr "remove"}}">{{svg "octicon-trash" 14}}</button>
</form>
</td>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="empty-placeholder"><p>{{ctx.Locale.Tr "org.settings.issue_types_empty"}}</p></div>
{{end}}
<div class="divider"></div>
<h5>{{ctx.Locale.Tr "org.settings.issue_type_add"}}</h5>
<form class="ui form" method="post" action="{{.OrgLink}}/settings/issue-types">
{{.CsrfTokenHtml}}
<div class="three fields">
<div class="required field">
<label>{{ctx.Locale.Tr "org.settings.issue_type_name"}}</label>
<input name="name" required placeholder="e.g. Bug, Feature, Task">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.issue_type_color"}}</label>
<input name="color" type="color" value="#2563eb">
</div>
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.issue_type_sort_order"}}</label>
<input name="sort_order" type="number" value="0" min="0">
</div>
</div>
<div class="two fields">
<div class="field">
<label>{{ctx.Locale.Tr "org.settings.issue_type_description"}}</label>
<input name="description" placeholder="Help text">
</div>
<div class="field">
<div class="ui checkbox tw-mt-4">
<input name="is_default" type="checkbox">
<label>{{ctx.Locale.Tr "org.settings.issue_type_default"}}</label>
</div>
</div>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "org.settings.issue_type_add"}}</button>
</form>
</div>
{{template "org/settings/layout_footer" .}}
+3
View File
@@ -37,6 +37,9 @@
<a class="{{if .PageIsSettingsIssuePriorities}}active {{end}}item" href="{{.OrgLink}}/settings/issue-priorities">
{{svg "octicon-flame"}} {{ctx.Locale.Tr "org.settings.issue_priorities"}}
</a>
<a class="{{if .PageIsSettingsIssueTypes}}active {{end}}item" href="{{.OrgLink}}/settings/issue-types">
{{svg "octicon-tag"}} {{ctx.Locale.Tr "org.settings.issue_types"}}
</a>
{{if .EnableActions}}
<details class="item toggleable-item" {{if or .PageIsOrgSettingsActionsGeneral .PageIsSharedSettingsRunners .PageIsSharedSettingsSecrets .PageIsSharedSettingsVariables}}open{{end}}>
<summary>{{svg "octicon-play"}} {{ctx.Locale.Tr "actions.actions"}}</summary>
+9
View File
@@ -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,33 @@
{{if .IssueTypeDefs}}
<div class="divider"></div>
<div class="tw-flex tw-items-center tw-justify-between tw-gap-2">
<span class="text grey tw-text-sm">{{ctx.Locale.Tr "repo.issues.type"}}</span>
{{$canModify := and .FieldEditFlags .FieldEditFlags.CustomFields}}
{{if $canModify}}
<form method="post" action="{{.RepoLink}}/issues/{{.Issue.ID}}/custom-type" class="tw-inline">
{{$.CsrfTokenHtml}}
<select name="type_id" class="ui compact mini dropdown tw-max-w-48" onchange="this.form.submit()">
<option value="0">-</option>
{{range .IssueTypeDefs}}
<option value="{{.ID}}" {{if eq .ID $.Issue.TypeID}}selected{{end}}
{{if .Color}}style="border-left: 3px solid {{.Color}}"{{end}}>
{{.Name}}
</option>
{{end}}
</select>
</form>
{{else}}
{{$found := false}}
{{range .IssueTypeDefs}}
{{if eq .ID $.Issue.TypeID}}
{{if .Color}}<span class="tw-inline-block tw-w-3 tw-h-3 tw-rounded" style="background-color: {{.Color}}"></span>{{end}}
<span class="tw-text-sm">{{.Name}}</span>
{{$found = true}}
{{end}}
{{end}}
{{if not $found}}
<span class="tw-text-sm text grey">-</span>
{{end}}
{{end}}
</div>
{{end}}
@@ -9,6 +9,8 @@
{{template "repo/issue/sidebar/issue_priority" $}}
{{template "repo/issue/sidebar/issue_type" $}}
{{template "repo/issue/sidebar/custom_fields" $}}
{{template "repo/issue/sidebar/milestone_list" $.IssuePageMetaData}}
+99
View File
@@ -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" .}}
-4
View File
@@ -19,10 +19,6 @@
<input name="org" value="{{.Manifest.Org}}" placeholder="Organization">
</div>
</div>
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_description"}}</label>
<input name="description" value="{{.Manifest.Description}}" placeholder="Project description">
</div>
<div class="three fields">
<div class="field">
<label>{{ctx.Locale.Tr "repo.settings.manifest_version"}}</label>
+3
View File
@@ -18,6 +18,9 @@
<a class="{{if .PageIsSettingsMetadata}}active {{end}}item" href="{{.RepoLink}}/settings/metadata">
{{svg "octicon-list-unordered"}} {{ctx.Locale.Tr "repo.settings.metadata"}}
</a>
<a class="{{if .PageIsSettingsSecurity}}active {{end}}item" href="{{.RepoLink}}/settings/security">
{{svg "octicon-shield"}} {{ctx.Locale.Tr "repo.settings.security"}}
</a>
{{if or .Repository.IsPrivate .Permission.HasAnyUnitPublicAccess}}
<a class="{{if .PageIsSettingsPublicAccess}}active {{end}}item" href="{{.RepoLink}}/settings/public_access">
{{svg "octicon-eye"}} {{ctx.Locale.Tr "repo.settings.public_access"}}
+140
View File
@@ -0,0 +1,140 @@
{{template "repo/settings/layout_head" (dict "ctxData" . "pageClass" "repository settings security")}}
<h4 class="ui top attached header">
{{svg "octicon-shield" 16 "tw-mr-2"}}{{ctx.Locale.Tr "repo.settings.security"}}
</h4>
<div class="ui attached segment">
<p class="text grey">{{ctx.Locale.Tr "repo.settings.security_desc"}}</p>
{{if .AlertCounts}}
<div class="tw-flex tw-gap-3 tw-mb-4">
{{range $sev, $count := .AlertCounts}}
<div class="ui mini {{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}}
<form class="ui form" method="post" action="{{.RepoLink}}/settings/security">
{{.CsrfTokenHtml}}
<h5 class="ui dividing header">{{ctx.Locale.Tr "repo.settings.security_scanners"}}</h5>
<div class="inline field">
<div class="ui checkbox">
<input name="enabled" type="checkbox" {{if .ScannerConfig.Enabled}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.security_enabled"}}</label>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="secret_scanner" type="checkbox" {{if .ScannerConfig.SecretScanner}}checked{{end}}>
<label>{{svg "octicon-key" 14}} {{ctx.Locale.Tr "repo.settings.security_secret_scanner"}}</label>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="depend_scanner" type="checkbox" {{if .ScannerConfig.DependScanner}}checked{{end}}>
<label>{{svg "octicon-package" 14}} {{ctx.Locale.Tr "repo.settings.security_depend_scanner"}}</label>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="code_scanner" type="checkbox" {{if .ScannerConfig.CodeScanner}}checked{{end}}>
<label>{{svg "octicon-code" 14}} {{ctx.Locale.Tr "repo.settings.security_code_scanner"}}</label>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="config_scanner" type="checkbox" {{if .ScannerConfig.ConfigScanner}}checked{{end}}>
<label>{{svg "octicon-gear" 14}} {{ctx.Locale.Tr "repo.settings.security_config_scanner"}}</label>
</div>
</div>
<div class="inline field">
<div class="ui checkbox">
<input name="license_scanner" type="checkbox" {{if .ScannerConfig.LicenseScanner}}checked{{end}}>
<label>{{svg "octicon-law" 14}} {{ctx.Locale.Tr "repo.settings.security_license_scanner"}}</label>
</div>
</div>
<div class="divider"></div>
<div class="inline field">
<div class="ui checkbox">
<input name="block_on_push" type="checkbox" {{if .ScannerConfig.BlockOnPush}}checked{{end}}>
<label>{{ctx.Locale.Tr "repo.settings.security_block_on_push"}}</label>
</div>
<p class="help">{{ctx.Locale.Tr "repo.settings.security_block_on_push_help"}}</p>
</div>
<button class="ui primary button" type="submit">{{ctx.Locale.Tr "repo.settings.security_save"}}</button>
</form>
</div>
<h4 class="ui top attached header tw-mt-4">
{{ctx.Locale.Tr "repo.settings.security_alerts"}}
<form method="post" action="{{.RepoLink}}/settings/security/scan" class="tw-float-right tw-inline">
{{.CsrfTokenHtml}}
<button class="ui mini primary button" type="submit">{{svg "octicon-sync" 14}} {{ctx.Locale.Tr "repo.settings.security_scan_now"}}</button>
</form>
</h4>
<div class="ui attached segment">
{{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>
<th></th>
</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}}
<code class="tw-text-xs">{{.FilePath}}{{if .LineNumber}}:{{.LineNumber}}{{end}}</code>
{{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>
<td class="tw-text-right">
{{if eq .Status "active"}}
<form method="post" action="{{$.RepoLink}}/settings/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}}/settings/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>
</tr>
{{end}}
</tbody>
</table>
{{else}}
<div class="empty-placeholder">
<p>{{ctx.Locale.Tr "repo.settings.security_no_alerts"}}</p>
</div>
{{end}}
</div>
{{template "repo/settings/layout_footer" .}}
+3
View File
@@ -26,6 +26,9 @@
{{range .Labels}}
<a href="?q={{$.Keyword}}&type={{$.ViewType}}&state={{$.State}}&labels={{.ID}}{{if ne $.listType "milestone"}}&milestone={{$.MilestoneID}}{{end}}&assignee={{$.AssigneeID}}&poster={{$.PosterID}}{{if $.ShowArchivedLabels}}&archived=true{{end}}">{{ctx.RenderUtils.RenderLabel .}}</a>
{{end}}
{{if and .TypeID $.IssueTypeDefs}}{{range $.IssueTypeDefs}}{{if eq .ID $.TypeID}}<span class="ui mini label" {{if .Color}}style="background-color: {{.Color}}; color: white"{{end}}>{{.Name}}</span>{{end}}{{end}}{{end}}
{{if and .PriorityID $.IssuePriorityDefs}}{{range $.IssuePriorityDefs}}{{if eq .ID $.PriorityID}}<span class="ui mini label" {{if .Color}}style="background-color: {{.Color}}; color: white"{{end}}>{{.Name}}</span>{{end}}{{end}}{{end}}
{{if and .StatusID $.IssueStatusDefs}}{{range $.IssueStatusDefs}}{{if eq .ID $.StatusID}}<span class="ui mini label" {{if .Color}}style="background-color: {{.Color}}; color: white"{{end}}>{{.Name}}</span>{{end}}{{end}}{{end}}
</span>
</div>
{{if .TotalTrackedTime}}
+4
View File
@@ -23,6 +23,10 @@
</div>
<button class="ui primary fluid button tw-mt-2" type="submit">{{ctx.Locale.Tr "sign_in"}}</button>
</form>
{{if or .OAuth2Providers .EnableSSPI}}
<div class="divider"></div>
{{template "user/auth/external_auth_methods" .}}
{{end}}
</div>
{{end}}
</div>
+21
View File
@@ -11,6 +11,27 @@
<a class="tw-block tw-my-4" href="{{.NotFoundGoBackURL}}">{{ctx.Locale.Tr "go_back"}}</a>
{{end}}
</div>
{{if not .IsSigned}}
<div class="tw-max-w-sm tw-mx-auto tw-mt-4">
<form class="ui form" action="{{AppSubUrl}}/user/login" method="post">
{{.CsrfTokenHtml}}
<input type="hidden" name="redirect_to" value="{{.CurrentURL}}">
<div class="required field">
<label>{{ctx.Locale.Tr "home.uname_holder"}}</label>
<input type="text" name="user_name" required autofocus>
</div>
<div class="required field">
<label>{{ctx.Locale.Tr "password"}}</label>
<input type="password" name="password" required>
</div>
<button class="ui primary fluid button tw-mt-2" type="submit">{{ctx.Locale.Tr "sign_in"}}</button>
</form>
{{if or .OAuth2Providers .EnableSSPI}}
<div class="divider"></div>
{{template "user/auth/external_auth_methods" .}}
{{end}}
</div>
{{end}}
</div>
</div>
</div>
+6 -6
View File
@@ -9,29 +9,29 @@
<div class="ui secondary vertical filter menu tw-bg-transparent">
<a class="{{if eq .ViewType "your_repositories"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "your_repositories"}}">
{{ctx.Locale.Tr "home.issues.in_your_repos"}}
<strong>{{CountFmt .IssueStats.YourRepositoriesCount}}</strong>
<span class="ui small label">{{CountFmt .IssueStats.YourRepositoriesCount}}</span>
</a>
<a class="{{if eq .ViewType "assigned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "assigned"}}">
{{ctx.Locale.Tr "repo.issues.filter_type.assigned_to_you"}}
<strong>{{CountFmt .IssueStats.AssignCount}}</strong>
<span class="ui small label">{{CountFmt .IssueStats.AssignCount}}</span>
</a>
<a class="{{if eq .ViewType "created_by"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "created_by"}}">
{{ctx.Locale.Tr "repo.issues.filter_type.created_by_you"}}
<strong>{{CountFmt .IssueStats.CreateCount}}</strong>
<span class="ui small label">{{CountFmt .IssueStats.CreateCount}}</span>
</a>
{{if .PageIsPulls}}
<a class="{{if eq .ViewType "review_requested"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "review_requested"}}">
{{ctx.Locale.Tr "repo.issues.filter_type.review_requested"}}
<strong>{{CountFmt .IssueStats.ReviewRequestedCount}}</strong>
<span class="ui small label">{{CountFmt .IssueStats.ReviewRequestedCount}}</span>
</a>
<a class="{{if eq .ViewType "reviewed_by"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "reviewed_by"}}">
{{ctx.Locale.Tr "repo.issues.filter_type.reviewed_by_you"}}
<strong>{{CountFmt .IssueStats.ReviewedCount}}</strong>
<span class="ui small label">{{CountFmt .IssueStats.ReviewedCount}}</span>
</a>
{{end}}
<a class="{{if eq .ViewType "mentioned"}}active{{end}} item" href="{{QueryBuild $queryLink "type" "mentioned"}}">
{{ctx.Locale.Tr "repo.issues.filter_type.mentioning_you"}}
<strong>{{CountFmt .IssueStats.MentionCount}}</strong>
<span class="ui small label">{{CountFmt .IssueStats.MentionCount}}</span>
</a>
</div>
</div>
+29 -10
View File
@@ -28,23 +28,42 @@ Each status has:
| Sort Order | Controls display order in dropdowns (ascending) |
| Is Active | Inactive statuses are hidden from dropdowns but preserved on existing issues |
### Example Statuses
### Default Statuses (auto-seeded)
| Status | Color | Closes Issue | Use Case |
|--------|-------|:------------:|----------|
| In Progress | Blue | No | Work is actively being done |
| Needs Info | Yellow | No | Waiting for more information from reporter |
| Blocked | Red | No | Cannot proceed due to external dependency |
| Won't Fix | Gray | Yes | Decided not to address this issue |
| Duplicate | Purple | Yes | Already tracked in another issue |
| Resolved | Green | Yes | Fix has been implemented and verified |
| Needs Info | Yellow | No | Waiting for more information |
| Blocked | Red | No | Cannot proceed due to dependency |
| Resolved | Green | Yes | Fix implemented and verified |
| Won't Fix | Gray | Yes | Decided not to address |
| Duplicate | Purple | Yes | Already tracked elsewhere |
| Pending: Design | Lavender | No | Waiting on design work |
| Pending: Testing | Yellow | No | Waiting for testing |
| Pending: Review | Green | No | Waiting for code review |
| Pending: Feedback | Pink | No | Waiting for feedback |
| Pending: Documentation | Purple | No | Waiting for docs |
| Pending: Deployment | Blue | No | Ready to deploy |
| Pending: Dependency | Light Blue | No | Blocked by external dependency |
Statuses are auto-seeded when an org first accesses them. Admins can add, edit, reorder, or deactivate statuses.
## Comment Form Integration
The status dropdown **replaces the close/reopen button** in the comment form footer for issues with org statuses:
- Open issues show all statuses plus a "Close" option
- Selecting a status with `closes_issue = true` auto-closes the issue
- Closed issues show only "Reopen" for non-admin users
- Admins see the full dropdown on closed issues including "Reopen"
- PRs still use the standard close/reopen button
## Issue List Badges
Status shows as a colored badge on each issue in the issue list view, alongside Type and Priority badges.
## Issue Sidebar
When an organization has custom statuses defined, a **Status** dropdown appears in the issue sidebar between labels and custom fields. The dropdown:
- Shows all active status definitions for the repo's organization
- Auto-submits on change (no save button needed)
Status also appears as a read-only display in the sidebar (the editable control is in the comment form). The dropdown:
- Displays a colored left border on each option
- Shows a power symbol on statuses that close the issue
- Selecting "—" (empty) clears the status
+7 -3
View File
@@ -7,7 +7,7 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
| **Language** | Go |
| **License** | MIT |
| **Upstream** | Gitea 1.26.1 |
| **Version** | v1.26.1-moko.06.07.03 |
| **Version** | v1.26.1-moko.06.11.01 |
| **Platform** | [Gitea](https://git.mokoconsulting.tech/MokoConsulting/MokoGitea) |
---
@@ -16,10 +16,13 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
- **Commercial License System** — Package-based license keys with download gating, domain restriction, key expiry, and payment webhook API
- **Update Server** — Built-in update feeds for Joomla, WordPress, Dolibarr, Composer, Drupal, PrestaShop, and WHMCS
- **Custom Issue Statuses** — Org-defined workflow states (In Progress, Blocked, Won't Fix) with auto close/reopen
- **First-Class Issue Fields** — Type (12 types), Status (13 statuses), Priority (4 levels) as built-in fields with auto-seed defaults, colored badges in issue list, and sidebar dropdowns
- **Security Scanning** — Built-in security scanner with secret detection (15 patterns), push-time scanning, alert management, and Security tab in repo navigation
- **Custom Fields** — Org-level field definitions for issues (sidebar) and repos (metadata) with dropdown, text, number, date, checkbox, and URL types
- **Manifest Settings** — Per-repo identity/governance/build metadata with REST API for CI/CD integration
- **Manifest Settings** — Per-repo identity/governance/build metadata with REST API and auto-sync on push
- **Wiki Folders** — Hierarchical folder navigation with sidebar tree, breadcrumbs, and index page fallback
- **Well-Known File Tabs** — README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG tabs on repo home page
- **MCP Server** — 120+ tool MCP server published to npm (@mokoconsulting/mokogitea-mcp) with SSE transport
- **Org-Level Branch Protection** — Organization-scoped rulesets that cascade to all repos. Supports glob patterns. Full CRUD API
- **Enterprise Sub-Orgs** — Parent-child organization hierarchy
- **Three-Level Visibility** — Public (200), Private (403), Hidden (404) for repositories
@@ -39,6 +42,7 @@ Moko Consulting's custom fork of [Gitea](https://gitea.com), extending the self-
| [Org Branch Protection API](Org-Branch-Protection-API) | Org-level branch protection rulesets and API reference |
| [Project API](Project-API) | Custom API endpoint reference for project boards |
| [Roadmap](Roadmap) | Development roadmap and planned features |
| [features/](features) | Feature documentation folder |
---
+24 -17
View File
@@ -1,42 +1,49 @@
# MokoGitea Roadmap
## Recently Completed (v1.26.1-moko.06.07)
## Recently Completed (v1.26.1-moko.06.11)
- First-class Type field (12 types) replacing labels and custom fields
- First-class Status field (13 statuses) with auto close/reopen
- First-class Priority field (4 levels) with auto-seed defaults
- All org labels migrated to first-class fields and deleted
- Type/Status/Priority colored badges in issue list view
- Security scanning platform with 15 secret detection patterns
- Security tab in repo navigation (admin-only)
- Wiki hierarchical folder navigation with sidebar tree
- Well-known file tabs (README/LICENSE/CONTRIBUTING/SECURITY/CHANGELOG)
- Custom issue statuses with auto close/reopen
- Org-level issue priorities (Critical/High/Medium/Low)
- Repo manifest settings with REST API
- Manifest auto-sync on push to default branch
- Status dropdown replaces close button
- Auto-seed default statuses and priorities for orgs
- MCP server published to npm (@mokoconsulting/mokogitea-mcp)
- MCP SSE transport for hosted deployments
- Repo manifest settings with REST API and auto-sync on push
- MCP server published to npm (@mokoconsulting/mokogitea-mcp) with SSE transport
- Dashboard issue count badges fixed
- Status dropdown replaces close/reopen button
- Org settings page for Issue Types
- MCP SSE endpoint hosted at git.mokoconsulting.tech/mcp/
- npm auto-publish workflow on MCP source changes
- OAuth providers on 403/404 error pages
- All stale branches cleaned up (main + dev only)
## In Progress
- Rename moko-platform to MokoPlatform
- Granular role-based permissions for all features (#9)
- Built-in secret scanning (#508)
- Enterprise Wiki with hierarchical folder navigation (#79)
- Wire moko-platform CLI to manifest API (#505)
- Bulk migrate remaining 41 flat wikis to folders
## Planned
- Standard status presets and cross-org migration (#507)
- Auto-create default teams on org creation (#513)
- Update server reads from repo_manifest (#512)
- Dependency vulnerability scanner module
- Code security analysis scanner module
- Payment gateways for license keys (#135)
- Independent visibility controls for issues/wiki/projects (#133)
## Infrastructure
- MCP SSE endpoint at git.mokoconsulting.tech/mcp
- MCP SSE endpoint hosted at git.mokoconsulting.tech/mcp
- Smithery/Claude Code marketplace listing
- Docker image for MCP server
- npm auto-publish workflow on release
---
| Revision | Date | Author | Description |
|---|---|---|---|
| 3.0 | 2026-06-06 | Jonathan Miller (@jmiller) | First-class fields, security scanning, wiki folders, MCP release |
| 2.0 | 2026-06-06 | Jonathan Miller (@jmiller) | Complete rewrite with current features and priorities |
| 1.0 | 2026-05-09 | Jonathan Miller (@jmiller) | Initial version |