diff --git a/.github/workflows/eslint.yml b/.github/workflows/eslint.yml index ef74b77..2ca1d11 100644 --- a/.github/workflows/eslint.yml +++ b/.github/workflows/eslint.yml @@ -12,6 +12,7 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + security-events: write steps: - name: Checkout code uses: actions/checkout@v6 @@ -25,5 +26,29 @@ jobs: - name: Install dependencies run: npm ci - - name: Run ESLint + - name: Install SARIF formatter + run: npm install --no-save @microsoft/eslint-formatter-sarif + + - name: Run ESLint → SARIF + continue-on-error: true + run: npx eslint . --format @microsoft/eslint-formatter-sarif --output-file eslint-results.sarif + + # GitHub Code Scanning honoriert `suppressions[]` mit leerer Justification nicht. + # Wir filtern in-source-suppressed Results raus, damit `/* eslint-disable-next-line */` + # tatsächlich auch in Code Scanning gilt. + - name: Strip suppressed results from SARIF + if: always() + run: | + jq '.runs[].results |= map(select((.suppressions // []) | length == 0))' \ + eslint-results.sarif > eslint-filtered.sarif + mv eslint-filtered.sarif eslint-results.sarif + + - name: Upload SARIF to Code Scanning + if: always() + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: eslint-results.sarif + category: eslint + + - name: Run ESLint (fail step on issues) run: npx eslint . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7324198..4f4304d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,27 @@ on: push: tags: - 'v*' # Trigger bei: v1.0.0, v1.2.3, etc. + workflow_dispatch: + inputs: + tag: + description: 'Tag, der released werden soll (z. B. v1.2.3)' + required: true + type: string + create_tag: + description: 'Neuen Tag erstellen, falls er noch nicht existiert' + required: false + type: boolean + default: false + ref: + description: 'Branch/Commit für den neuen Tag (nur falls oben aktiviert)' + required: false + type: string + default: main + +# Verhindert, dass sich zwei Release-Läufe für denselben Tag überschneiden +concurrency: + group: release-${{ inputs.tag || github.ref_name }} + cancel-in-progress: false jobs: release: @@ -12,48 +33,93 @@ jobs: contents: write steps: + # Tag ermitteln — funktioniert sowohl bei Tag-Push als auch bei manueller Ausführung + - name: Tag ermitteln + id: tag + run: | + set -euo pipefail + if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + TAG="${{ inputs.tag }}" + else + TAG="${GITHUB_REF_NAME}" + fi + if [[ ! "$TAG" =~ ^v ]]; then + echo "::error::Tag '$TAG' entspricht nicht dem erwarteten Muster 'v*'." + exit 1 + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + + # Checkout des Tags (falls vorhanden) bzw. des angegebenen Branches (für neue Tags). + # fetch-depth: 0 holt komplette History + alle Tags. - name: Checkout uses: actions/checkout@v6 with: - fetch-depth: 0 # Komplette Git-History für Diff + ref: ${{ inputs.ref || github.ref }} + fetch-depth: 0 + + # Prüft, ob der Tag existiert. Falls nicht: + # - create_tag aktiviert → Tag wird aus 'ref' erstellt und gepusht + # - create_tag deaktiviert → Abbruch mit Hinweis + - name: Tag prüfen & ggf. erstellen + run: | + set -euo pipefail + TAG="${{ steps.tag.outputs.tag }}" + + if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then + echo "✅ Tag '$TAG' existiert bereits." + exit 0 + fi + + if [ "${{ inputs.create_tag }}" != "true" ]; then + echo "::error::Tag '$TAG' existiert nicht. Workflow erneut ausführen und die Option 'Neuen Tag erstellen' aktivieren — der Tag wird dann aus '${{ inputs.ref }}' erstellt." + exit 1 + fi + + echo "🏷️ Tag '$TAG' existiert nicht — wird aus '${{ inputs.ref }}' erstellt." + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git tag -a "$TAG" -m "Release $TAG" + git push origin "refs/tags/$TAG" + echo "✅ Tag '$TAG' erstellt und gepusht." - name: Modified files & Commits ermitteln - id: changes run: | - PREV_TAG=$(git describe --tags --abbrev=0 HEAD^ 2>/dev/null || echo "") - CURR_TAG="${GITHUB_REF_NAME}" + set -euo pipefail + CURR_TAG="${{ steps.tag.outputs.tag }}" + PREV_TAG=$(git describe --tags --abbrev=0 "${CURR_TAG}^" 2>/dev/null || echo "") + REPO="${GITHUB_REPOSITORY}" if [ -z "$PREV_TAG" ]; then - RANGE="HEAD" - echo -e "> ⚠️ First Release – no comparison with previous tag possible.\n" > release_body.md + RANGE="$CURR_TAG" + printf '> ⚠️ First Release – no comparison with previous tag possible.\n\n' > release_body.md else RANGE="$PREV_TAG..$CURR_TAG" - echo -e "> 🔍 Comparison: [\`$PREV_TAG\` → \`$CURR_TAG\`](https://github.com/MSK-Scripts/msk-shortener/compare/$PREV_TAG...$CURR_TAG)\n" > release_body.md + printf '> 🔍 Comparison: [`%s` → `%s`](https://github.com/%s/compare/%s...%s)\n\n' \ + "$PREV_TAG" "$CURR_TAG" "$REPO" "$PREV_TAG" "$CURR_TAG" > release_body.md fi # Commits - REPO="MSK-Scripts/msk-shortener" echo "## 📝 Commits" >> release_body.md - git log $RANGE --pretty=format:"%H %s (%an)" | while read hash rest; do - SHORT=$(echo $hash | cut -c1-7) - echo "- [\`$SHORT\`](https://github.com/$REPO/commit/$hash) $rest" >> release_body.md + git log "$RANGE" --pretty=format:"%h %H %s (%an)" | while read -r short hash rest; do + echo "- [\`$short\`](https://github.com/$REPO/commit/$hash) $rest" >> release_body.md done # Modified files - echo -e "\n\n## 📂 Modified files" >> release_body.md + printf '\n\n## 📂 Modified files\n' >> release_body.md echo '```' >> release_body.md - git diff --name-status $RANGE \ + git diff --name-status "$RANGE" \ | sed 's/^A\t/✅ NEW /;s/^M\t/✏️ CHANGED /;s/^D\t/🗑️ DELETED /' \ >> release_body.md echo '```' >> release_body.md # Statistics - STATS=$(git diff --shortstat $RANGE 2>/dev/null || echo "First Release") - echo -e "\n---\n📊 **$STATS**" >> release_body.md + STATS=$(git diff --shortstat "$RANGE" 2>/dev/null || echo "First Release") + printf '\n---\n📊 **%s**\n' "$STATS" >> release_body.md - name: GitHub Release erstellen uses: softprops/action-gh-release@v3 with: + tag_name: ${{ steps.tag.outputs.tag }} body_path: release_body.md generate_release_notes: true token: ${{ secrets.GITHUB_TOKEN }} diff --git a/components/BackgroundDialog.tsx b/components/BackgroundDialog.tsx index 379241c..00c1145 100644 --- a/components/BackgroundDialog.tsx +++ b/components/BackgroundDialog.tsx @@ -3,6 +3,7 @@ import { useEffect, useState, useTransition } from 'react'; import { createPortal } from 'react-dom'; import { useBoard } from '@/store/boardStore'; import { useMounted } from '@/lib/useMounted'; +import { safeHttpUrl } from '@/lib/safeUrl'; type Props = { onClose: () => void; @@ -55,7 +56,7 @@ export function BackgroundDialog({ onClose }: Props) { if (!mounted) return null; const preview = input.trim(); - const previewValid = preview && isValidHttpUrl(preview); + const previewSrc = safeHttpUrl(preview); const save = () => { setError(null); @@ -156,13 +157,13 @@ export function BackgroundDialog({ onClose }: Props) { - {previewValid && ( + {previewSrc && (
Vorschau
$1');
diff --git a/lib/discord.ts b/lib/discord.ts
index 39f952a..08fe110 100644
--- a/lib/discord.ts
+++ b/lib/discord.ts
@@ -206,8 +206,29 @@ export class DiscordRateLimitError extends Error {
}
}
+// Baut eine vollständige Discord-API-URL und verhindert Host-Wechsel durch User-Input.
+// `:` (Scheme-Separator), `..` (Path-Traversal) und führendes `//` (protokoll-relativ)
+// sind alle verboten — damit kann `new URL(path, base)` den Host nicht überschreiben.
+const DISCORD_API_ORIGIN = 'https://discord.com';
+const DISCORD_API_BASE = 'https://discord.com/api/v10/';
+
+function buildDiscordUrl(path: string): URL {
+ if (path.length === 0 || path[0] !== '/') {
+ throw new Error(`Invalid Discord path: ${path}`);
+ }
+ if (path.includes(':') || path.includes('..') || path.startsWith('//')) {
+ throw new Error(`Invalid Discord path (unsafe characters): ${path}`);
+ }
+ // Slice führendes `/` ab — sonst würde absolute-path den base-Pfad ersetzen.
+ const url = new URL(path.slice(1), DISCORD_API_BASE);
+ if (url.origin !== DISCORD_API_ORIGIN) {
+ throw new Error(`Invalid Discord path (host escape): ${path}`);
+ }
+ return url;
+}
+
async function discordGet