-
Notifications
You must be signed in to change notification settings - Fork 11.1k
合并 #6418
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
合并 #6418
Changes from all commits
5c47d89
28d3045
aebda55
7aa1f42
ca7a0dc
8991a01
5955359
c7665b8
9dbd655
cc9ba5b
e1c6690
21c1f69
30e43df
65bbbea
2a98072
668bc10
d2d8852
e38cd96
3dc337d
ac3779a
03f1ecc
5e30730
bdcd8ea
56ad272
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,155 @@ | ||
| name: Publish custom image to GHCR | ||
|
|
||
| on: | ||
| push: | ||
| tags: | ||
| - 'v*' | ||
| workflow_dispatch: | ||
| inputs: | ||
| image_tag: | ||
| description: 'Docker image tag only (e.g. v1.0.0-custom), not a git ref' | ||
| required: true | ||
| type: string | ||
|
|
||
| env: | ||
| REGISTRY: ghcr.io | ||
|
|
||
| permissions: | ||
| contents: read | ||
| packages: write | ||
|
|
||
| jobs: | ||
| prepare: | ||
| name: Prepare build metadata | ||
| runs-on: ubuntu-latest | ||
| outputs: | ||
| image_name: ${{ steps.meta.outputs.image_name }} | ||
| tag: ${{ steps.meta.outputs.tag }} | ||
| steps: | ||
| - id: meta | ||
| run: | | ||
| echo "image_name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" | ||
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | ||
| TAG="${{ github.event.inputs.image_tag }}" | ||
| else | ||
| TAG=${GITHUB_REF#refs/tags/} | ||
| fi | ||
| echo "tag=${TAG}" >> "$GITHUB_OUTPUT" | ||
|
|
||
| build_single_arch: | ||
| name: Build & push (${{ matrix.arch }}) | ||
| needs: [prepare] | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| include: | ||
| - arch: amd64 | ||
| platform: linux/amd64 | ||
| runner: ubuntu-latest | ||
| - arch: arm64 | ||
| platform: linux/arm64 | ||
| runner: ubuntu-24.04-arm | ||
| runs-on: ${{ matrix.runner }} | ||
| outputs: | ||
| tag: ${{ needs.prepare.outputs.tag }} | ||
|
|
||
| permissions: | ||
| contents: read | ||
| packages: write | ||
|
|
||
| steps: | ||
| - name: Check out | ||
| uses: actions/checkout@v4 | ||
| with: | ||
| fetch-depth: 0 | ||
|
Comment on lines
+61
to
+64
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
echo "## workflow excerpt"
sed -n '1,120p' .github/workflows/docker-ghcr-custom.yml 2>/dev/null || true
echo
echo "## dockerignore files"
fd -a '.dockerignore$|Dockerfile$|compose.*' . | sed 's#^\./##' | sort | head -200
echo
echo "## .dockerignore contents (if any)"
while IFS= read -r f; do
echo "--- $f"
cat -n "$f" | sed -n '1,200p'
done < <(fd '.dockerignore$' .)
echo
echo "## repo .git directory status"
python3 - <<'PY'
from pathlib import Path
p = Path('.git')
print('exists=', p.exists() or p.is_symlink(), 'is_dir=', p.is_dir(), 'is_symlink=', p.is_symlink())
if p.exists():
print('contents=', sorted(str(x) for x in p.iterdir() if x.name not in {'config.example'})[:50])
PY
echo
echo "## checks for persist-credentials and build context usage"
rg -n "persist-credentials|docker/build-push-action|context:|\.dockerignore" .github/workflows/docker-ghcr-custom.yml .github/workflows || trueRepository: QuantumNous/new-api Length of output: 3850 🌐 Web query:
💡 Result: In Citations:
🌐 Web query:
💡 Result: The.git directory is not automatically excluded from the build context by the.dockerignore file, though it is handled differently depending on how the build context is provided [1][2][3]. When building from a local directory, the.dockerignore file does not exclude the.git directory by default [2][4]. If you wish to exclude.git from your build context, you must explicitly add it to your.dockerignore file [3]. Using the pattern /.git ensures that all.git directories within your project, including those in subdirectories, are excluded from the build context [3]. When building directly from a remote Git repository URL, Docker BuildKit automatically excludes the.git directory by default [1]. You can override this behavior if necessary by using the --keep-git-dir=true flag [1]. In summary: - Local builds: You must explicitly add /.git to your.dockerignore file if you want to exclude it [3]. - Remote Git repository builds: The.git directory is excluded by default [1]. It is a common best practice to include.git in your.dockerignore file to reduce the size of the build context and prevent sensitive version control metadata from being included in your image [2][4]. Citations:
Prevent
🧰 Tools🪛 zizmor (1.26.1)[warning] 61-64: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false (artipacked) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
|
|
||
| - name: Write VERSION | ||
| run: | | ||
| TAG="${{ needs.prepare.outputs.tag }}" | ||
| echo "TAG=${TAG}" >> "$GITHUB_ENV" | ||
| echo "${TAG}" > VERSION | ||
| echo "Building tag: ${TAG} for ${{ matrix.arch }}" | ||
| echo "Image: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.image_name }}" | ||
|
|
||
| - name: Set up Docker Buildx | ||
| uses: docker/setup-buildx-action@v3 | ||
|
|
||
| - name: Log in to GHCR | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| registry: ${{ env.REGISTRY }} | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Extract metadata (labels) | ||
| id: meta | ||
| uses: docker/metadata-action@v5 | ||
| with: | ||
| images: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.image_name }} | ||
|
|
||
| - name: Build & push | ||
| id: build | ||
| uses: docker/build-push-action@v6 | ||
| with: | ||
| context: . | ||
| platforms: ${{ matrix.platform }} | ||
| push: true | ||
| tags: | | ||
| ${{ env.REGISTRY }}/${{ needs.prepare.outputs.image_name }}:${{ env.TAG }}-${{ matrix.arch }} | ||
| ${{ env.REGISTRY }}/${{ needs.prepare.outputs.image_name }}:latest-${{ matrix.arch }} | ||
| labels: ${{ steps.meta.outputs.labels }} | ||
| cache-from: type=gha | ||
| cache-to: type=gha,mode=max | ||
|
|
||
| - name: Image summary | ||
| run: | | ||
| echo "### Docker Image (${{ matrix.arch }})" >> "$GITHUB_STEP_SUMMARY" | ||
| echo '```' >> "$GITHUB_STEP_SUMMARY" | ||
| echo "${{ env.REGISTRY }}/${{ needs.prepare.outputs.image_name }}:${TAG}-${{ matrix.arch }}" >> "$GITHUB_STEP_SUMMARY" | ||
| echo "${{ steps.build.outputs.digest }}" >> "$GITHUB_STEP_SUMMARY" | ||
| echo '```' >> "$GITHUB_STEP_SUMMARY" | ||
|
|
||
| create_manifests: | ||
| name: Create multi-arch manifests | ||
| needs: [prepare, build_single_arch] | ||
| runs-on: ubuntu-latest | ||
|
|
||
| permissions: | ||
| contents: read | ||
| packages: write | ||
|
|
||
| steps: | ||
| - name: Set version | ||
| run: | | ||
| echo "TAG=${{ needs.prepare.outputs.tag }}" >> "$GITHUB_ENV" | ||
| echo "IMAGE_NAME=${{ needs.prepare.outputs.image_name }}" >> "$GITHUB_ENV" | ||
|
|
||
| - name: Log in to GHCR | ||
| uses: docker/login-action@v3 | ||
| with: | ||
| registry: ${{ env.REGISTRY }} | ||
| username: ${{ github.actor }} | ||
| password: ${{ secrets.GITHUB_TOKEN }} | ||
|
|
||
| - name: Create & push manifest (version) | ||
| run: | | ||
| docker buildx imagetools create \ | ||
| -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}" \ | ||
| "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}-amd64" \ | ||
| "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}-arm64" | ||
|
|
||
| - name: Create & push manifest (latest) | ||
| run: | | ||
| docker buildx imagetools create \ | ||
| -t "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" \ | ||
| "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-amd64" \ | ||
| "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest-arm64" | ||
|
|
||
| - name: Manifest summary | ||
| run: | | ||
| echo "### Multi-arch Manifest" >> "$GITHUB_STEP_SUMMARY" | ||
| echo '```' >> "$GITHUB_STEP_SUMMARY" | ||
| echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}" >> "$GITHUB_STEP_SUMMARY" | ||
| echo "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> "$GITHUB_STEP_SUMMARY" | ||
| docker buildx imagetools inspect "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${TAG}" >> "$GITHUB_STEP_SUMMARY" | ||
| echo '```' >> "$GITHUB_STEP_SUMMARY" | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| package controller | ||
|
|
||
| import ( | ||
| "net/http" | ||
| "sort" | ||
|
|
||
| "github.com/QuantumNous/new-api/common" | ||
| "github.com/QuantumNous/new-api/model" | ||
| "github.com/QuantumNous/new-api/service" | ||
| "github.com/QuantumNous/new-api/setting/console_setting" | ||
| "github.com/QuantumNous/new-api/setting/ratio_setting" | ||
|
|
||
| "github.com/gin-gonic/gin" | ||
| ) | ||
|
|
||
| type extensionsAvailabilityGroup struct { | ||
| Group string `json:"group"` | ||
| Records []model.GroupAvailabilityRecord `json:"records"` | ||
| SuccessRate float64 `json:"success_rate"` | ||
| AvgUseTime float64 `json:"avg_use_time"` | ||
| Status string `json:"status"` | ||
| Total int `json:"total"` | ||
| SuccessCount int `json:"success_count"` | ||
| } | ||
|
|
||
| func GetExtensionsAvailability(c *gin.Context) { | ||
| isAdmin := c.GetInt("role") >= common.RoleAdminUser | ||
| if !console_setting.IsAvailabilityMonitorVisible(isAdmin) { | ||
| c.JSON(http.StatusForbidden, gin.H{ | ||
| "success": false, | ||
| "message": "availability monitor is not available", | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| userId := c.GetInt("id") | ||
| userGroup, _ := model.GetUserGroup(userId, false) | ||
| userUsableGroups := service.GetUserUsableGroups(userGroup) | ||
|
|
||
| groupNames := make([]string, 0) | ||
| for groupName := range ratio_setting.GetGroupRatioCopy() { | ||
| // Match GetUserGroups: only billing groups the user can select (skip "auto"). | ||
| if groupName == "auto" { | ||
| continue | ||
| } | ||
| if _, ok := userUsableGroups[groupName]; !ok { | ||
| continue | ||
| } | ||
| groupNames = append(groupNames, groupName) | ||
| } | ||
| sort.Strings(groupNames) | ||
|
|
||
| groups := make([]extensionsAvailabilityGroup, 0, len(groupNames)) | ||
| for _, groupName := range groupNames { | ||
| records, err := model.GetRecentGroupAvailabilityLogs(groupName, 100) | ||
| if err != nil { | ||
| common.ApiError(c, err) | ||
| return | ||
| } | ||
| okCount := 0 | ||
| successUseTimeSum := 0 | ||
| for _, record := range records { | ||
| if record.Ok { | ||
| okCount++ | ||
| successUseTimeSum += record.UseTime | ||
| } | ||
| } | ||
| successRate, avgUseTime, status := console_setting.SummarizeAvailabilityRecords( | ||
| okCount, | ||
| len(records), | ||
| successUseTimeSum, | ||
| ) | ||
| groups = append(groups, extensionsAvailabilityGroup{ | ||
| Group: groupName, | ||
| Records: records, | ||
| SuccessRate: successRate, | ||
| AvgUseTime: avgUseTime, | ||
| Status: status, | ||
| Total: len(records), | ||
| SuccessCount: okCount, | ||
| }) | ||
| } | ||
|
Comment on lines
+53
to
+82
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win Per-request N+1 DB query pattern with no caching or rate limiting. Each call fans out into one Since per-group log data is identical across users (only the exposed group set differs by permission), a short-TTL cache (e.g., keyed by group name, refreshed every few seconds) shared across requests would eliminate most of the duplicate work without touching per-user filtering logic. 🤖 Prompt for AI Agents |
||
|
|
||
| common.ApiSuccess(c, gin.H{ | ||
| "groups": groups, | ||
| }) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,83 @@ | ||
| package controller | ||
|
|
||
| import ( | ||
| "fmt" | ||
| "net/http" | ||
|
|
||
| "github.com/QuantumNous/new-api/common" | ||
| "github.com/QuantumNous/new-api/logger" | ||
| "github.com/QuantumNous/new-api/model" | ||
| "github.com/QuantumNous/new-api/setting/operation_setting" | ||
| "github.com/gin-gonic/gin" | ||
| ) | ||
|
|
||
| // GetLotteryStatus 获取抽奖状态 | ||
| func GetLotteryStatus(c *gin.Context) { | ||
| if !operation_setting.IsLotteryEnabled() { | ||
| common.ApiErrorMsg(c, "抽奖功能未启用") | ||
| return | ||
| } | ||
| userId := c.GetInt("id") | ||
| data, err := model.GetUserLotteryState(userId) | ||
| if err != nil { | ||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": false, | ||
| "message": err.Error(), | ||
| }) | ||
| return | ||
| } | ||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": true, | ||
| "data": data, | ||
| }) | ||
| } | ||
|
|
||
| type lotteryDrawRequest struct { | ||
| BetUSD float64 `json:"bet_usd"` | ||
| } | ||
|
|
||
| // DoLottery 执行抽奖 | ||
| func DoLottery(c *gin.Context) { | ||
| if !operation_setting.IsLotteryEnabled() { | ||
| common.ApiErrorMsg(c, "抽奖功能未启用") | ||
| return | ||
| } | ||
|
|
||
| var req lotteryDrawRequest | ||
| if err := c.ShouldBindJSON(&req); err != nil { | ||
| req.BetUSD = 0 | ||
| } | ||
|
|
||
| userId := c.GetInt("id") | ||
| result, err := model.UserLotteryDraw(userId, req.BetUSD, c.ClientIP()) | ||
| if err != nil { | ||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": false, | ||
| "message": err.Error(), | ||
| }) | ||
| return | ||
| } | ||
|
|
||
| usdDelta := operation_setting.QuotaToUsd(result.Draw.QuotaDelta) | ||
| msg := fmt.Sprintf("老虎机抽奖:%s,额度变化 %s(约 $%.4f)", result.Draw.PrizeName, logger.LogQuota(result.Draw.QuotaDelta), usdDelta) | ||
| model.RecordLog(userId, model.LogTypeSystem, msg) | ||
|
|
||
| c.JSON(http.StatusOK, gin.H{ | ||
| "success": true, | ||
| "message": "抽奖成功", | ||
| "data": gin.H{ | ||
| "prize_index": result.Draw.PrizeIndex, | ||
| "prize_name": result.Draw.PrizeName, | ||
| "quota_delta": result.Draw.QuotaDelta, | ||
| "usd_delta": usdDelta, | ||
| "bet_quota": result.Draw.BetQuota, | ||
| "bet_usd": operation_setting.QuotaToUsd(result.Draw.BetQuota), | ||
| "is_thanks": result.Draw.IsThanks, | ||
| "is_pity": result.Draw.IsPity, | ||
| "is_thursday": result.Draw.IsThursday, | ||
| "remaining_pool": result.RemainingPool, | ||
| "remaining_pool_usd": operation_setting.QuotaToUsd(result.RemainingPool), | ||
| "draw_date": result.Draw.DrawDate, | ||
| }, | ||
| }) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Untrusted input interpolated directly into a shell script (script injection).
TAG="${{ github.event.inputs.image_tag }}"splices aworkflow_dispatchstring input directly into arun:block instead of passing it viaenv:. A craftedimage_tagvalue containing shell metacharacters (e.g.`...`or$(...)) can execute arbitrary commands in the runner, which holdspackages: writecredentials. This tainted value then propagates throughprepare's output and is reused via further${{ }}interpolation in laterrun:steps (e.g. Lines 68, 72, 108-109, 124-125, 137-139, 144-146, 152-154), so hardening at the source doesn't automatically fix the downstream reuses — eachrun:step should consume the value via anenv:-mapped shell variable.🔒 Proposed fix at the source
steps: - id: meta + env: + IMAGE_TAG_INPUT: ${{ github.event.inputs.image_tag }} run: | echo "image_name=$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_OUTPUT" if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - TAG="${{ github.event.inputs.image_tag }}" + TAG="$IMAGE_TAG_INPUT" else TAG=${GITHUB_REF#refs/tags/} fi echo "tag=${TAG}" >> "$GITHUB_OUTPUT"Apply the same
env:-mapping pattern (e.g.env: TAG: ${{ needs.prepare.outputs.tag }}then reference$TAG) to the otherrun:steps that currently interpolate${{ needs.prepare.outputs.tag }}/${{ needs.prepare.outputs.image_name }}directly.📝 Committable suggestion
🧰 Tools
🪛 zizmor (1.26.1)
[error] 33-33: code injection via template expansion (template-injection): may expand into attacker-controllable code
(template-injection)
🤖 Prompt for AI Agents
Source: Linters/SAST tools