diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index e06016a..045e9c1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,7 +35,7 @@ kics: image: deps.global.bsf.tools/docker/python:3.12-slim timeout: 10m before_script: - - pip install httpx + - pip install --index-url "$PIP_INDEX_URL" httpx script: - | python - <<'PYTEST_SCRIPT' @@ -175,7 +175,7 @@ frontend_tests: # Shared deploy configuration .deploy_template: &deploy_template stage: deploy - needs: [build_image, kics, hadolint, python_tests, frontend_tests] + needs: [build_image, kics, hadolint, python_tests, frontend_tests, secrets] image: deps.global.bsf.tools/registry-1.docker.io/alpine/k8s:1.29.12 .helm_setup: &helm_setup @@ -245,6 +245,7 @@ deploy_stage: -f $VALUES_FILE \ --set image.tag=git.linux-amd64-$CI_COMMIT_SHA \ --wait \ + --atomic \ --timeout 5m - kubectl rollout status deployment/orchard-stage-server -n $NAMESPACE --timeout=5m - *verify_deployment @@ -280,6 +281,7 @@ deploy_feature: --set minioIngress.host=minio-$CI_COMMIT_REF_SLUG.common.global.bsf.tools \ --set minioIngress.tls.secretName=minio-$CI_COMMIT_REF_SLUG-tls \ --wait \ + --atomic \ --timeout 5m - kubectl rollout status deployment/orchard-$CI_COMMIT_REF_SLUG-server -n $NAMESPACE --timeout=5m - export BASE_URL="https://orchard-$CI_COMMIT_REF_SLUG.common.global.bsf.tools" diff --git a/.gitleaksignore b/.gitleaksignore index d47191d..c57613c 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -5,3 +5,9 @@ # These are historical commits - files have since been deleted or updated with inline comments 7e68baed0886a3c928644cd01aa3b39f92d4f976:backend/tests/test_duplicate_detection.py:generic-api-key:154 2f1891cf0126ec0e7d4c789d872a2cb2dd3a1745:backend/tests/unit/test_storage.py:generic-api-key:381 +10d36947948de796f0bacea3827f4531529c405d:backend/tests/unit/test_storage.py:generic-api-key:381 +bccbc71c13570d14b8b26a11335c45f102fe3072:backend/tests/unit/test_storage.py:generic-api-key:381 +5c9da9003b844a2d655cce74a7c82c57e74f27c4:backend/tests/unit/test_storage.py:generic-api-key:381 +90bb2a3a393d2361dc3136ee8d761debb0726d8a:backend/tests/unit/test_storage.py:generic-api-key:381 +37666e41a72d2a4f34447c0d1a8728e1d7271d24:backend/tests/unit/test_storage.py:generic-api-key:381 +0cc4f253621a9601c5193f6ae1e7ae33f0e7fc9b:backend/tests/unit/test_storage.py:generic-api-key:381 diff --git a/CHANGELOG.md b/CHANGELOG.md index 39db477..17172b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,13 +16,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added internal proxy configuration for npm, pip, helm, and apt (#51) ### Changed +- Added `--atomic` flag to Helm deployments for automatic rollback on failure - Adjusted dark mode color palette to use lighter background tones for better readability and reduced eye strain (#52) +- Replaced project card grid with sortable data table on Home page for better handling of large project lists +- Replaced package card grid with sortable data table on Project page for consistency +- Replaced SortDropdown with table header sorting on Package page for consistency +- Enabled sorting on supported table columns (name, created, updated) via clickable headers +- Updated browser tab title to "Orchard" with custom favicon - Improved pod naming: Orchard pods now named `orchard-{env}-server-*` for clarity (#51) ### Fixed - Fixed `cleanup_feature` job failing when branch is deleted (`GIT_STRATEGY: none`) (#51) - Fixed gitleaks false positives with fingerprints for historical commits (#51) - Fixed integration tests running when deploy fails (`when: on_success`) (#51) +- Fixed static file serving for favicon and other files in frontend dist root +- Fixed deploy jobs running when secrets scan fails (added `secrets` to deploy dependencies) +- Fixed dev environment memory requests to equal limits per cluster Kyverno policy +- Fixed init containers missing resource limits (Kyverno policy compliance) ### Removed - Removed unused `store_streaming()` method from storage.py (#51) diff --git a/backend/app/main.py b/backend/app/main.py index f733e54..ac71491 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -88,6 +88,11 @@ if os.path.exists(static_dir): raise HTTPException(status_code=404, detail="Not found") + # Check if requesting a static file from dist root (favicon, etc.) + static_file_path = os.path.join(static_dir, full_path) + if os.path.isfile(static_file_path) and not full_path.startswith("."): + return FileResponse(static_file_path) + # Serve SPA for all other routes (including /project/*) index_path = os.path.join(static_dir, "index.html") if os.path.exists(index_path): diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 3792e3e..6426494 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -6,7 +6,7 @@ services: context: . dockerfile: Dockerfile.local ports: - - "127.0.0.1:8080:8080" + - "0.0.0.0:8080:8080" environment: - ORCHARD_SERVER_HOST=0.0.0.0 - ORCHARD_SERVER_PORT=8080 @@ -71,10 +71,6 @@ services: networks: - orchard-network restart: unless-stopped - security_opt: - - no-new-privileges:true - cap_drop: - - ALL deploy: resources: limits: @@ -100,10 +96,6 @@ services: networks: - orchard-network restart: unless-stopped - security_opt: - - no-new-privileges:true - cap_drop: - - ALL deploy: resources: limits: @@ -124,10 +116,6 @@ services: " networks: - orchard-network - security_opt: - - no-new-privileges:true - cap_drop: - - ALL deploy: resources: limits: @@ -149,10 +137,6 @@ services: networks: - orchard-network restart: unless-stopped - security_opt: - - no-new-privileges:true - cap_drop: - - ALL deploy: resources: limits: diff --git a/frontend/index.html b/frontend/index.html index b14dd1b..33af6ba 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -2,9 +2,9 @@ - + - Orchard - Content-Addressable Storage + Orchard
diff --git a/frontend/public/orchard.svg b/frontend/public/orchard.svg new file mode 100644 index 0000000..dfa3b76 --- /dev/null +++ b/frontend/public/orchard.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/components/DataTable.css b/frontend/src/components/DataTable.css index 1716990..e7d5dfe 100644 --- a/frontend/src/components/DataTable.css +++ b/frontend/src/components/DataTable.css @@ -98,3 +98,58 @@ text-overflow: ellipsis; white-space: nowrap; } + +/* Clickable rows */ +.data-table__row--clickable { + cursor: pointer; +} + +.data-table__row--clickable:hover { + background: var(--bg-hover); +} + +/* Responsive table wrapper */ +.data-table--responsive { + overflow-x: auto; + -webkit-overflow-scrolling: touch; +} + +.data-table--responsive table { + min-width: 800px; +} + +/* Cell with name and icon */ +.data-table .cell-name { + display: flex; + align-items: center; + gap: 8px; + font-weight: 500; + color: var(--text-primary); +} + +.data-table .cell-name:hover { + color: var(--accent-primary); +} + +/* Date cells */ +.data-table .cell-date { + color: var(--text-tertiary); + font-size: 0.8125rem; + white-space: nowrap; +} + +/* Description cell */ +.data-table .cell-description { + max-width: 300px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-secondary); + font-size: 0.875rem; +} + +/* Owner cell */ +.data-table .cell-owner { + color: var(--text-secondary); + font-size: 0.875rem; +} diff --git a/frontend/src/components/DataTable.tsx b/frontend/src/components/DataTable.tsx index dc8e048..23d5896 100644 --- a/frontend/src/components/DataTable.tsx +++ b/frontend/src/components/DataTable.tsx @@ -18,6 +18,7 @@ interface DataTableProps { onSort?: (key: string) => void; sortKey?: string; sortOrder?: 'asc' | 'desc'; + onRowClick?: (item: T) => void; } export function DataTable({ @@ -29,6 +30,7 @@ export function DataTable({ onSort, sortKey, sortOrder, + onRowClick, }: DataTableProps) { if (data.length === 0) { return ( @@ -71,7 +73,11 @@ export function DataTable({ {data.map((item) => ( - + onRowClick?.(item)} + className={onRowClick ? 'data-table__row--clickable' : ''} + > {columns.map((column) => ( {column.render(item)} diff --git a/frontend/src/pages/Home.css b/frontend/src/pages/Home.css index f5891d8..6a2f176 100644 --- a/frontend/src/pages/Home.css +++ b/frontend/src/pages/Home.css @@ -1,6 +1,6 @@ /* Page Layout */ .home { - max-width: 1000px; + max-width: 1200px; margin: 0 auto; } diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index 6d45faf..fb6aeab 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -1,9 +1,9 @@ import { useState, useEffect, useCallback } from 'react'; -import { Link, useSearchParams } from 'react-router-dom'; +import { Link, useSearchParams, useNavigate } from 'react-router-dom'; import { Project, PaginatedResponse } from '../types'; import { listProjects, createProject } from '../api'; import { Badge } from '../components/Badge'; -import { SortDropdown, SortOption } from '../components/SortDropdown'; +import { DataTable } from '../components/DataTable'; import { FilterDropdown, FilterOption } from '../components/FilterDropdown'; import { FilterChip, FilterChipGroup } from '../components/FilterChip'; import { Pagination } from '../components/Pagination'; @@ -20,12 +20,6 @@ function LockIcon() { ); } -const SORT_OPTIONS: SortOption[] = [ - { value: 'name', label: 'Name' }, - { value: 'created_at', label: 'Created' }, - { value: 'updated_at', label: 'Updated' }, -]; - const VISIBILITY_OPTIONS: FilterOption[] = [ { value: '', label: 'All Projects' }, { value: 'public', label: 'Public Only' }, @@ -34,6 +28,7 @@ const VISIBILITY_OPTIONS: FilterOption[] = [ function Home() { const [searchParams, setSearchParams] = useSearchParams(); + const navigate = useNavigate(); const { user } = useAuth(); const [projectsData, setProjectsData] = useState | null>(null); @@ -101,8 +96,10 @@ function Home() { } } - const handleSortChange = (newSort: string, newOrder: 'asc' | 'desc') => { - updateParams({ sort: newSort, order: newOrder, page: '1' }); + const handleSortChange = (columnKey: string) => { + // Toggle order if clicking the same column, otherwise default to asc + const newOrder = columnKey === sort ? (order === 'asc' ? 'desc' : 'asc') : 'asc'; + updateParams({ sort: columnKey, order: newOrder, page: '1' }); }; const handleVisibilityChange = (value: string) => { @@ -189,7 +186,6 @@ function Home() { value={visibility} onChange={handleVisibilityChange} /> - {hasActiveFilters && ( @@ -204,69 +200,106 @@ function Home() { )} - {projects.length === 0 ? ( -
- {hasActiveFilters ? ( -

No projects match your filters. Try adjusting your search.

- ) : ( -

No projects yet. Create your first project to get started!

- )} -
- ) : ( - <> -
- {projects.map((project) => ( - -

+
+ project.id} + onRowClick={(project) => navigate(`/project/${project.name}`)} + onSort={handleSortChange} + sortKey={sort} + sortOrder={order} + emptyMessage={ + hasActiveFilters + ? 'No projects match your filters. Try adjusting your search.' + : 'No projects yet. Create your first project to get started!' + } + columns={[ + { + key: 'name', + header: 'Name', + sortable: true, + render: (project) => ( + {!project.is_public && } {project.name} -

- {project.description &&

{project.description}

} -
-
- - {project.is_public ? 'Public' : 'Private'} - - {user && project.access_level && ( - + ), + }, + { + key: 'description', + header: 'Description', + className: 'cell-description', + render: (project) => project.description || '—', + }, + { + key: 'visibility', + header: 'Visibility', + render: (project) => ( + + {project.is_public ? 'Public' : 'Private'} + + ), + }, + { + key: 'created_by', + header: 'Owner', + className: 'cell-owner', + render: (project) => project.created_by, + }, + ...(user + ? [ + { + key: 'access_level', + header: 'Access', + render: (project: Project) => + project.access_level ? ( + - {project.is_owner ? 'Owner' : project.access_level.charAt(0).toUpperCase() + project.access_level.slice(1)} - - )} -
-
- Created {new Date(project.created_at).toLocaleDateString()} - {project.updated_at !== project.created_at && ( - Updated {new Date(project.updated_at).toLocaleDateString()} - )} -
-
-
- by {project.created_by} -
- - ))} -
+ : project.access_level === 'admin' + ? 'success' + : project.access_level === 'write' + ? 'info' + : 'default' + } + > + {project.is_owner + ? 'Owner' + : project.access_level.charAt(0).toUpperCase() + project.access_level.slice(1)} + + ) : ( + '—' + ), + }, + ] + : []), + { + key: 'created_at', + header: 'Created', + sortable: true, + className: 'cell-date', + render: (project) => new Date(project.created_at).toLocaleDateString(), + }, + { + key: 'updated_at', + header: 'Updated', + sortable: true, + className: 'cell-date', + render: (project) => new Date(project.updated_at).toLocaleDateString(), + }, + ]} + /> + - {pagination && pagination.total_pages > 1 && ( - - )} - + {pagination && pagination.total_pages > 1 && ( + )} ); diff --git a/frontend/src/pages/PackagePage.tsx b/frontend/src/pages/PackagePage.tsx index 76284b2..698e5e5 100644 --- a/frontend/src/pages/PackagePage.tsx +++ b/frontend/src/pages/PackagePage.tsx @@ -5,7 +5,6 @@ import { listTags, getDownloadUrl, getPackage, getMyProjectAccess, UnauthorizedE import { Breadcrumb } from '../components/Breadcrumb'; import { Badge } from '../components/Badge'; import { SearchInput } from '../components/SearchInput'; -import { SortDropdown, SortOption } from '../components/SortDropdown'; import { FilterChip, FilterChipGroup } from '../components/FilterChip'; import { DataTable } from '../components/DataTable'; import { Pagination } from '../components/Pagination'; @@ -14,11 +13,6 @@ import { useAuth } from '../contexts/AuthContext'; import './Home.css'; import './PackagePage.css'; -const SORT_OPTIONS: SortOption[] = [ - { value: 'name', label: 'Name' }, - { value: 'created_at', label: 'Created' }, -]; - function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const k = 1024; @@ -164,8 +158,9 @@ function PackagePage() { updateParams({ search: value, page: '1' }); }; - const handleSortChange = (newSort: string, newOrder: 'asc' | 'desc') => { - updateParams({ sort: newSort, order: newOrder, page: '1' }); + const handleSortChange = (columnKey: string) => { + const newOrder = columnKey === sort ? (order === 'asc' ? 'desc' : 'asc') : 'asc'; + updateParams({ sort: columnKey, order: newOrder, page: '1' }); }; const handlePageChange = (newPage: number) => { @@ -198,19 +193,19 @@ function PackagePage() { ), }, { - key: 'size', + key: 'artifact_size', header: 'Size', render: (t: TagDetail) => {formatBytes(t.artifact_size)}, }, { - key: 'content_type', + key: 'artifact_content_type', header: 'Type', render: (t: TagDetail) => ( {t.artifact_content_type || '-'} ), }, { - key: 'original_name', + key: 'artifact_original_name', header: 'Filename', className: 'cell-truncate', render: (t: TagDetail) => ( @@ -376,7 +371,6 @@ function PackagePage() { placeholder="Filter tags..." className="list-controls__search" /> - {hasActiveFilters && ( @@ -385,25 +379,21 @@ function PackagePage() { )} - t.id} - emptyMessage={ - hasActiveFilters - ? 'No tags match your filters. Try adjusting your search.' - : 'No tags yet. Upload an artifact with a tag to create one!' - } - onSort={(key) => { - if (key === sort) { - handleSortChange(key, order === 'asc' ? 'desc' : 'asc'); - } else { - handleSortChange(key, 'asc'); +
+ t.id} + emptyMessage={ + hasActiveFilters + ? 'No tags match your filters. Try adjusting your search.' + : 'No tags yet. Upload an artifact with a tag to create one!' } - }} - sortKey={sort} - sortOrder={order} - /> + onSort={handleSortChange} + sortKey={sort} + sortOrder={order} + /> +
{pagination && pagination.total_pages > 1 && ( { - updateParams({ sort: newSort, order: newOrder, page: '1' }); + const handleSortChange = (columnKey: string) => { + const newOrder = columnKey === sort ? (order === 'asc' ? 'desc' : 'asc') : 'asc'; + updateParams({ sort: columnKey, order: newOrder, page: '1' }); }; const handleFormatChange = (value: string) => { @@ -294,7 +289,6 @@ function ProjectPage() { ))} - {hasActiveFilters && ( @@ -304,70 +298,78 @@ function ProjectPage() { )} - {packages.length === 0 ? ( -
- {hasActiveFilters ? ( -

No packages match your filters. Try adjusting your search.

- ) : ( -

No packages yet. Create your first package to start uploading artifacts!

- )} -
- ) : ( - <> -
- {packages.map((pkg) => ( - -
-

{pkg.name}

- {pkg.format} -
- {pkg.description &&

{pkg.description}

} +
+ pkg.id} + onRowClick={(pkg) => navigate(`/project/${projectName}/${pkg.name}`)} + onSort={handleSortChange} + sortKey={sort} + sortOrder={order} + emptyMessage={ + hasActiveFilters + ? 'No packages match your filters. Try adjusting your search.' + : 'No packages yet. Create your first package to start uploading artifacts!' + } + columns={[ + { + key: 'name', + header: 'Name', + sortable: true, + render: (pkg) => {pkg.name}, + }, + { + key: 'description', + header: 'Description', + className: 'cell-description', + render: (pkg) => pkg.description || '—', + }, + { + key: 'format', + header: 'Format', + render: (pkg) => {pkg.format}, + }, + { + key: 'tag_count', + header: 'Tags', + render: (pkg) => pkg.tag_count ?? '—', + }, + { + key: 'artifact_count', + header: 'Artifacts', + render: (pkg) => pkg.artifact_count ?? '—', + }, + { + key: 'total_size', + header: 'Size', + render: (pkg) => + pkg.total_size !== undefined && pkg.total_size > 0 ? formatBytes(pkg.total_size) : '—', + }, + { + key: 'latest_tag', + header: 'Latest', + render: (pkg) => + pkg.latest_tag ? {pkg.latest_tag} : '—', + }, + { + key: 'created_at', + header: 'Created', + sortable: true, + className: 'cell-date', + render: (pkg) => new Date(pkg.created_at).toLocaleDateString(), + }, + ]} + /> +
- {(pkg.tag_count !== undefined || pkg.artifact_count !== undefined) && ( -
- {pkg.tag_count !== undefined && ( -
- {pkg.tag_count} - Tags -
- )} - {pkg.artifact_count !== undefined && ( -
- {pkg.artifact_count} - Artifacts -
- )} - {pkg.total_size !== undefined && pkg.total_size > 0 && ( -
- {formatBytes(pkg.total_size)} - Size -
- )} -
- )} - -
- {pkg.latest_tag && ( - - Latest: {pkg.latest_tag} - - )} - Created {new Date(pkg.created_at).toLocaleDateString()} -
- - ))} -
- - {pagination && pagination.total_pages > 1 && ( - - )} - + {pagination && pagination.total_pages > 1 && ( + )} {canAdmin && projectName && ( diff --git a/helm/orchard/templates/deployment.yaml b/helm/orchard/templates/deployment.yaml index 3a8c97b..1353547 100644 --- a/helm/orchard/templates/deployment.yaml +++ b/helm/orchard/templates/deployment.yaml @@ -37,12 +37,26 @@ spec: image: "{{ .Values.initContainer.image.repository }}:{{ .Values.initContainer.image.tag }}" imagePullPolicy: {{ .Values.initContainer.image.pullPolicy }} command: ['sh', '-c', 'until nc -z {{ include "orchard.postgresql.host" . }} 5432; do echo waiting for database; sleep 2; done;'] + resources: + limits: + cpu: 50m + memory: 32Mi + requests: + cpu: 10m + memory: 32Mi {{- end }} {{- if .Values.minio.enabled }} - name: wait-for-minio image: "{{ .Values.initContainer.image.repository }}:{{ .Values.initContainer.image.tag }}" imagePullPolicy: {{ .Values.initContainer.image.pullPolicy }} command: ['sh', '-c', 'until nc -z {{ .Release.Name }}-minio 9000; do echo waiting for minio; sleep 2; done;'] + resources: + limits: + cpu: 50m + memory: 32Mi + requests: + cpu: 10m + memory: 32Mi {{- end }} containers: - name: {{ .Chart.Name }} diff --git a/helm/orchard/values-dev.yaml b/helm/orchard/values-dev.yaml index 6dd6130..2b461df 100644 --- a/helm/orchard/values-dev.yaml +++ b/helm/orchard/values-dev.yaml @@ -53,13 +53,14 @@ ingress: - orchard-dev.common.global.bsf.tools # Overridden by CI # Lighter resources for ephemeral environments +# Note: memory requests must equal limits per cluster policy resources: limits: cpu: 250m memory: 256Mi requests: cpu: 100m - memory: 128Mi + memory: 256Mi livenessProbe: httpGet: @@ -127,6 +128,25 @@ postgresql: primary: persistence: enabled: false + # Resources with memory requests = limits per cluster policy + resourcesPreset: "none" + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 100m + memory: 256Mi + # Volume permissions init container + volumePermissions: + resourcesPreset: "none" + resources: + limits: + cpu: 50m + memory: 64Mi + requests: + cpu: 10m + memory: 64Mi # MinIO - ephemeral, no persistence minio: @@ -142,6 +162,35 @@ minio: defaultBuckets: "orchard-artifacts" persistence: enabled: false + # Resources with memory requests = limits per cluster policy + resourcesPreset: "none" # Disable preset to use explicit resources + resources: + limits: + cpu: 250m + memory: 256Mi + requests: + cpu: 100m + memory: 256Mi + # Init container resources + defaultInitContainers: + volumePermissions: + resourcesPreset: "none" + resources: + limits: + cpu: 50m + memory: 64Mi + requests: + cpu: 10m + memory: 64Mi + # Provisioning job resources + provisioning: + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 50m + memory: 128Mi # MinIO ingress - hostname overridden by CI minioIngress: diff --git a/helm/orchard/values-stage.yaml b/helm/orchard/values-stage.yaml index 9d370f5..c702bcb 100644 --- a/helm/orchard/values-stage.yaml +++ b/helm/orchard/values-stage.yaml @@ -136,6 +136,25 @@ postgresql: persistence: enabled: false size: 10Gi + # Resources with memory requests = limits per cluster policy + resourcesPreset: "none" + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 250m + memory: 512Mi + # Volume permissions init container + volumePermissions: + resourcesPreset: "none" + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 50m + memory: 128Mi # MinIO subchart configuration minio: @@ -152,6 +171,35 @@ minio: persistence: enabled: false size: 50Gi + # Resources with memory requests = limits per cluster policy + resourcesPreset: "none" # Disable preset to use explicit resources + resources: + limits: + cpu: 500m + memory: 512Mi + requests: + cpu: 250m + memory: 512Mi + # Init container resources + defaultInitContainers: + volumePermissions: + resourcesPreset: "none" + resources: + limits: + cpu: 100m + memory: 128Mi + requests: + cpu: 50m + memory: 128Mi + # Provisioning job resources + provisioning: + resources: + limits: + cpu: 200m + memory: 256Mi + requests: + cpu: 100m + memory: 256Mi # MinIO external ingress for presigned URL access (separate from subchart ingress) minioIngress: diff --git a/kics.config b/kics.config index 5572c19..31e8a6f 100644 --- a/kics.config +++ b/kics.config @@ -23,3 +23,29 @@ exclude-queries: # Reason: We intentionally don't pin curl version to get security updates. # This is documented with hadolint ignore comment in Dockerfile. - 965a08d7-ef86-4f14-8792-4a3b2098937e + + # Container Capabilities Unrestricted (MEDIUM) + # Reason: LOCAL DEVELOPMENT ONLY. Stock postgres, redis, minio images require + # certain capabilities (SETUID, SETGID, CHOWN) to switch users at startup. + # cap_drop: ALL breaks these containers. Production Kubernetes deployments + # use securityContext with appropriate settings. + - ce76b7d0-9e77-464d-b86f-c5c48e03e22d + + # No New Privileges Not Set (HIGH) + # Reason: LOCAL DEVELOPMENT ONLY. Stock postgres, redis, minio images need + # to escalate privileges during initialization (e.g., postgres switches from + # root to postgres user). no-new-privileges:true prevents this and causes + # containers to crash. Production Kubernetes deployments handle this via + # securityContext. + - 27fcc7d6-c49b-46e0-98f1-6c082a6a2750 + + # Security Opt Not Set (MEDIUM) + # Reason: LOCAL DEVELOPMENT ONLY. Related to above - security_opt is not set + # on database services because no-new-privileges breaks them. + - 610e266e-6c12-4bca-9925-1ed0cd29742b + + # Container Traffic Not Bound To Host Interface (MEDIUM) + # Reason: LOCAL DEVELOPMENT ONLY. The orchard-server port is bound to 0.0.0.0 + # to allow testing from other machines on the local network. This is only in + # docker-compose.local.yml, not production deployments. + - 451d79dc-0588-476a-ad03-3c7f0320abb3