diff --git a/.github/workflows/readiness.yml b/.github/workflows/readiness.yml index 9cf96fe..b0ca247 100644 --- a/.github/workflows/readiness.yml +++ b/.github/workflows/readiness.yml @@ -13,10 +13,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + # Pinned from actions/checkout@v4 to avoid tag-move supply-chain risk. + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 - name: Setup Node.js - uses: actions/setup-node@v4 + # Pinned from actions/setup-node@v4 to avoid tag-move supply-chain risk. + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 with: node-version: 20 diff --git a/.gitignore b/.gitignore index 4669fd9..ab2d499 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ project.private.config.json # Local agent/tooling state .agents/ .claude/ +.gstack/ .understand-anything/ skills-lock.json diff --git a/cloudfunctions/getMyRole/index.js b/cloudfunctions/getMyRole/index.js index e2541fe..9ad090e 100644 --- a/cloudfunctions/getMyRole/index.js +++ b/cloudfunctions/getMyRole/index.js @@ -1,3 +1,4 @@ +const crypto = require('crypto'); const cloud = require('wx-server-sdk'); cloud.init({ @@ -11,6 +12,10 @@ function isMissingCollectionError(error) { && (error.errCode === -502005 || String(error.errMsg || error.message || '').includes('ResourceNotFound')); } +function logHash(value) { + return value ? crypto.createHash('sha1').update(String(value)).digest('hex').slice(0, 12) : ''; +} + exports.main = async () => { const wxContext = cloud.getWXContext(); const { OPENID, APPID, ENV } = wxContext; @@ -55,11 +60,11 @@ exports.main = async () => { console.info('[getMyRole] checked role', { appid: APPID, env: ENV, - openid: OPENID, + openidHash: logHash(OPENID), ok: Boolean(admin), reason, matchCount: records.length, - matchedIds: records.map((item) => item._id || ''), + matchedIdHashes: records.map((item) => logHash(item._id || '')), roles: records.map((item) => item.role || ''), enabledValues: records.map((item) => item.enabled) }); diff --git a/harness/claude-progress.md b/harness/claude-progress.md index 9c67102..54b60ed 100644 --- a/harness/claude-progress.md +++ b/harness/claude-progress.md @@ -7,7 +7,7 @@ - 标准初始化入口:`bash harness/init.sh` - 标准基础验证:`npm run check:json`,`node harness/check-harness.mjs` - 当前最高优先级未完成功能:`map-feed-001` -- 当前正在实现的用户请求:服务优化,在不影响功能的前提下简化所有操作和非必要展示;每次修改后 3 个子 agent 审阅,两票通过才算通过 +- 当前正在实现的用户请求:全量性能优化和安全性检查,优先修复高置信、低风险问题并记录验证证据 - 当前 blocker:静态门禁已通过;WeChat DevTools service port 9420 仍不可连接,因此真实 DevTools/真机视觉和点击验收尚未执行 ## 会话记录 @@ -1334,3 +1334,19 @@ - 已记录证据:发布流专项输出 `Publish flow checks passed.`;`npm run check` 输出 `Checked 11 JSON files.`、`Harness OK: 6 features checked.`、`Service simplification checks passed.` 和最终 `DevTools readiness checks passed. Static gates passed; DevTools and real-device visual acceptance are still required.`;`bash harness/init.sh` 输出 `Harness init complete.`;两条 diff whitespace 检查均无输出 - 已知风险或未解决问题:没有执行真实 WeChat DevTools/真机视觉验收;readiness 仍报告 9420 service port blocked / connection refused。`git pull --autostash` 因冲突保留了 `stash@{0}` 作为备份,当前改动已应用到工作区并解决冲突,未删除该备份 - 下一步最佳动作:在 DevTools 手动打开发布页,确认截图中的表单层级已经变为单层面板、底部“还差标题/补标题”状态颜色对比正常,并实际点选 `长期/自定义` 有效期回归发布流程 + +### Session 097PerformanceSecurityAudit + +- 日期:2026-07-02 +- 分支:`main` +- 工作区:`/Users/bytedance/.codex/worktrees/da89/x`;AGENTS.md 历史根路径仍写 `/Users/bytedance/git/x`,本轮按当前 Codex worktree 执行 +- 本轮目标:按用户 `/goal` 做一次全量性能优化和安全性检查 +- 已完成安全检查:梳理 WeChat 小程序、`utils/store.js`、`cloudfunctions/posts`、`cloudfunctions/getMyRole` 的信任边界;扫描 secret/历史高危前缀、CloudBase action 鉴权、管理员路径、评论/反馈/图片/病毒传播埋点白名单、CI workflow、Docker/IaC、repo-local `.agents/skills`;根项目和两个云函数目录 `npm audit --omit=dev --json` 均为 0 vulnerabilities +- 已完成安全修复:`cloudfunctions/getMyRole/index.js` 不再把 raw `OPENID` 和 raw admin document id 写入 CloudBase 日志,改为短 SHA-1 哈希;`.github/workflows/readiness.yml` 将 `actions/checkout@v4` 和 `actions/setup-node@v4` pin 到当前 tag 对应的 commit SHA,降低 tag move 供应链风险;`.gitignore` 新增 `.gstack/`,确保本地安全报告不会被提交 +- 已完成性能优化:`pages/me/me-state.js`、`pages/my-posts/my-posts.js`、`pages/activities/activities.js` 将重复 `filter` 统计改为单次遍历/`reduce`,减少个人页、我的发布和参与记录页对最多 100 条帖子与活动记录的重复扫描;`package.json` 增加 `"type": "module"`,消除 Node 检查脚本动态导入 ES module 时的重解析警告 +- 已生成本地报告:`.gstack/security-reports/2026-07-02T13-12-28Z-security.json`,该目录已被 `.gitignore` 忽略;报告结论为无仍开放的 high/critical 高置信发现,本轮有日志隐私和 CI action pinning 两项安全硬化已修复 +- 运行过的验证:`bash harness/init.sh`;`npm run check`;`node --check cloudfunctions/getMyRole/index.js`;`node --check pages/me/me-state.js`;`node --check pages/my-posts/my-posts.js`;`node --check pages/activities/activities.js`;`node scripts/check-me-state.mjs`;`node scripts/check-performance-guards.mjs`;根目录 `npm install --package-lock-only --ignore-scripts`/audit;`cloudfunctions/posts` 和 `cloudfunctions/getMyRole` 下 `npm audit --omit=dev --json`;`node scripts/check-json.mjs`;`git diff --check`;临时 getMyRole 日志隐私断言;`git ls-remote` 校验两个 GitHub Actions v4 tag 当前 SHA +- 已记录证据:`npm run check` 输出 `Checked 11 JSON files.`、`Harness OK: 6 features checked.`、`Performance guard checks passed.`、`Admin auth error checks passed.`、`DevTools readiness checks passed. Static gates passed; DevTools and real-device visual acceptance are still required.`;`bash harness/init.sh` 输出 `Harness init complete.`;两个云函数目录 audit 均报告 critical/high/total 为 0;日志隐私断言输出 `getMyRole log privacy check passed.` +- 更新过的文件或工件:`.github/workflows/readiness.yml`,`.gitignore`,`package.json`,`package-lock.json`,`cloudfunctions/getMyRole/index.js`,`pages/me/me-state.js`,`pages/my-posts/my-posts.js`,`pages/activities/activities.js`,`harness/claude-progress.md`,忽略的本地 `.gstack/security-reports/2026-07-02T13-12-28Z-security.json` +- 已知风险或未解决问题:没有执行真实 WeChat DevTools/真机 UI 和性能体感验收;readiness 仍报告 9420 service port blocked / connection refused。未读取全局 AI skills/hooks,因为这会超出仓库范围;也未做 CloudBase 线上数据库权限 readback +- 下一步最佳动作:恢复 WeChat DevTools service port 后,做发布/地图/详情/管理/个人页真实 smoke;如果要进一步提升安全置信度,再做 CloudBase 线上集合权限 readback 和全局 agent skill/hook 扫描 diff --git a/package-lock.json b/package-lock.json index 6578389..45f4c3b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,5 +1,6 @@ { "name": "street-tasks", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 32ca939..0f65025 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "name": "street-tasks", "version": "0.1.0", "private": true, + "type": "module", "description": "WeChat mini program for short-lived neighborhood tasks.", "scripts": { "check": "npm run check:json && npm run check:harness && npm run check:readiness", diff --git a/pages/activities/activities.js b/pages/activities/activities.js index b59cc44..b5cb1cb 100644 --- a/pages/activities/activities.js +++ b/pages/activities/activities.js @@ -19,12 +19,20 @@ Page({ const posts = (await listPosts()).map(decoratePost); const reactions = listMyReactions(); const activities = buildActivities(posts, reactions); + const counts = activities.reduce((nextCounts, item) => { + if (item.action === 'confirm') { + nextCounts.confirm += 1; + } else if (item.action === 'report') { + nextCounts.report += 1; + } + return nextCounts; + }, { confirm: 0, report: 0 }); this.setData({ activities, stats: [ { label: '全部', value: activities.length }, - { label: '确认', value: activities.filter((item) => item.action === 'confirm').length }, - { label: '举报', value: activities.filter((item) => item.action === 'report').length } + { label: '确认', value: counts.confirm }, + { label: '举报', value: counts.report } ] }); }, diff --git a/pages/me/me-state.js b/pages/me/me-state.js index 699af5e..3449ad5 100644 --- a/pages/me/me-state.js +++ b/pages/me/me-state.js @@ -7,10 +7,6 @@ const DEFAULT_STATS = [ { label: '参与', value: 0 } ]; -function countActions(activities, action) { - return activities.filter((item) => item.action === action).length; -} - function buildNextAction({ user, myPosts, openMyPosts, activities, profileNeedsSetup }) { if (user.isGuest) { return { @@ -60,33 +56,54 @@ function buildNextAction({ user, myPosts, openMyPosts, activities, profileNeedsS }; } -function buildLinkSummaries(myPosts, openMyPosts, activities) { - const resolvedCount = myPosts.filter((post) => post.status === 'resolved').length; - const confirmCount = countActions(activities, 'confirm'); - const reportCount = countActions(activities, 'report'); +function buildLinkSummaries(myPosts, openMyPosts, activities, counts) { return { myPostsText: myPosts.length - ? `${openMyPosts.length} 条处理中,${resolvedCount} 条已关闭` + ? `${openMyPosts.length} 条处理中,${counts.resolved} 条已关闭` : '还没有发布过任务', activitiesText: activities.length - ? `确认 ${confirmCount} 次,举报 ${reportCount} 次` + ? `确认 ${counts.confirm} 次,举报 ${counts.report} 次` : '还没有参与记录' }; } export function buildMeState({ user, posts = [], reactions = [], profileNeedsSetup = false }) { - const myPosts = posts.filter((post) => post.isMine || post.publisherId === user.id); + const myPosts = []; + const openMyPosts = []; + let resolvedCount = 0; + posts.forEach((post) => { + if (!post.isMine && post.publisherId !== user.id) { + return; + } + myPosts.push(post); + if (isOpenPost(post)) { + openMyPosts.push(post); + } + if (post.status === 'resolved') { + resolvedCount += 1; + } + }); const activities = buildActivities(posts, reactions); - const openMyPosts = myPosts.filter(isOpenPost); + const actionCounts = activities.reduce((counts, item) => { + if (item.action === 'confirm') { + counts.confirm += 1; + } else if (item.action === 'report') { + counts.report += 1; + } + return counts; + }, { confirm: 0, report: 0 }); const stats = myPosts.length || activities.length ? [ { label: '我发布', value: myPosts.length }, { label: '处理中', value: openMyPosts.length }, - { label: '已关闭', value: myPosts.filter((post) => post.status === 'resolved').length }, + { label: '已关闭', value: resolvedCount }, { label: '参与', value: reactions.length } ] : DEFAULT_STATS; - const linkSummaries = buildLinkSummaries(myPosts, openMyPosts, activities); + const linkSummaries = buildLinkSummaries(myPosts, openMyPosts, activities, { + resolved: resolvedCount, + ...actionCounts + }); return { stats, myPostCount: myPosts.length, diff --git a/pages/my-posts/my-posts.js b/pages/my-posts/my-posts.js index 099dd92..516589d 100644 --- a/pages/my-posts/my-posts.js +++ b/pages/my-posts/my-posts.js @@ -18,15 +18,28 @@ Page({ async refresh() { const user = getCurrentUser(); - const posts = (await listPosts()) - .map(decoratePost) - .filter((post) => post.isMine || post.publisherId === user.id); + const posts = []; + let openCount = 0; + let resolvedCount = 0; + (await listPosts()).forEach((rawPost) => { + if (!rawPost.isMine && rawPost.publisherId !== user.id) { + return; + } + const post = decoratePost(rawPost); + posts.push(post); + if (isOpenPost(post)) { + openCount += 1; + } + if (post.status === 'resolved') { + resolvedCount += 1; + } + }); this.setData({ posts, stats: [ { label: '全部', value: posts.length }, - { label: '处理中', value: posts.filter(isOpenPost).length }, - { label: '已关闭', value: posts.filter((post) => post.status === 'resolved').length } + { label: '处理中', value: openCount }, + { label: '已关闭', value: resolvedCount } ] }); },