dsh-code-security 0.4.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,15 @@
4
4
 
5
5
  [历史英文记录](docs/CHANGELOG.en.md)
6
6
 
7
+ ## 0.5.0(2026-10-17)
8
+
9
+ - 设置面板顶部新增「首次使用」分步引导:一次只讲一步,支持上一步/下一步/跳过,看完记在本机不再出现。
10
+ - 引导内容与该插件的实际用法一致;可在开发者工具里清掉 `dsh-first-run:dsh-code-security:v1` 重新查看。
11
+ ## 0.4.1 (2026-10-15)
12
+
13
+ - 修复同一行同时命中高熵密钥和其他安全规则时,其他规则的证据仍带出原始字符串的问题;所有证据统一使用同一套高熵脱敏判断,也覆盖截断的字符串。
14
+ - 回归检查模型返回值、历史、已接受基线、SARIF 与 Markdown,保留原始证据指纹用于跟踪。
15
+
7
16
  ## 0.4.0 (2026-10-15)
8
17
 
9
18
  - 增加 DSH 中英文设置工作台:项目选择、扫描/增量/复查、覆盖情况、问题筛选、脱敏证据、复制位置与报告下载。
@@ -2,6 +2,10 @@
2
2
 
3
3
  本页整理原 README 的历史验证说明,保留当时的版本、日期与范围。自动测试、启动检查、浏览器操作和真实服务验收分别记录,不能相互替代。更详细的版本验收文件仍保留在仓库中。
4
4
 
5
+ ## 0.4.1(2026-10-15)
6
+
7
+ 79 项 Windows 测试全部通过,包含下列三个真实原生引擎。新增同一行同时命中注入与高熵密钥的回归,确认其他规则的证据、返回值、历史、基线和导出也不再带出原值;截断的字符串使用相同判断。其余功能与客户端沿用 0.4.0 的验收范围。
8
+
5
9
  ## 0.4.0(2026-10-15)
6
10
 
7
11
  本地环境:Windows、Node 24.16.0、官方 npm DSH 0.2.1-alpha.3。全部使用 E 盘隔离目录与合成数据,没有复制用户密钥或修改前台桌面窗口。
package/lib/client.js CHANGED
@@ -1,6 +1,57 @@
1
1
  // Browser source, distributed directly; the TypeScript build preserves this file.
2
2
  window.__ModuleLoader__.load({id:'dsh-code-security',factory:require=>{
3
3
  const React=require('react'),{useState,useEffect,useRef,useMemo}=React,h=React.createElement;
4
+ var __FR_KEY = "dsh-first-run:dsh-code-security:v1";
5
+ var __FR_TITLE = "代码安全 · 首次使用(4 步)";
6
+ var __FR_STEPS = [{"title":"先全仓扫一次","body":"点「扫描」或让助手跑一次全仓扫描建立基线;第一次结果可能很多,属正常。"},{"title":"之后只看增量","body":"日常用「审 diff」,只报本次改动新增的问题,噪音低得多。"},{"title":"修完复验","body":"修完后点复验,插件会对比上次结果,告诉你哪些真的关掉了。"},{"title":"基线要谨慎","body":"「接受基线」会把当前问题标记为已知,之后不再拦截;只在确认无害时使用。"}];
7
+ // ---- first-run guide (shared snippet) ----
8
+ var __FR_STYLE_ID = 'dsh-first-run-css';
9
+ var __FR_CSS = [
10
+ '.fr-card{border:1px solid var(--dsw-border,rgba(0,0,0,.10));border-radius:12px;padding:14px 16px;margin:0 0 14px;background:linear-gradient(180deg,rgba(59,130,246,.06),rgba(59,130,246,.02))}',
11
+ '.fr-head{display:flex;align-items:center;gap:8px;margin-bottom:6px}',
12
+ '.fr-dot{width:6px;height:6px;border-radius:50%;background:var(--dsw-accent,#3b82f6)}',
13
+ '.fr-title{font-weight:650;font-size:13.5px}',
14
+ '.fr-count{margin-left:auto;font-size:11px;color:var(--dsw-text-weak,#6b7280);font-variant-numeric:tabular-nums}',
15
+ '.fr-step{font-weight:600;font-size:13px;margin:2px 0 4px}',
16
+ '.fr-body{color:var(--dsw-text-weak,#4b5563);font-size:12.5px;line-height:1.65}',
17
+ '.fr-try{margin-top:8px;padding:7px 9px;border-radius:8px;background:rgba(59,130,246,.09);font-size:12px;display:flex;gap:8px}',
18
+ '.fr-try code{font-family:ui-monospace,monospace;white-space:pre-wrap;word-break:break-word}',
19
+ '.fr-bar{display:flex;gap:8px;align-items:center;margin-top:12px}',
20
+ '.fr-progress{display:flex;gap:4px;margin-right:auto}',
21
+ '.fr-seg{width:16px;height:3px;border-radius:2px;background:var(--dsw-border,rgba(0,0,0,.12))}',
22
+ '.fr-seg.on{background:var(--dsw-accent,#3b82f6)}',
23
+ '.fr-btn{border:1px solid var(--dsw-border,rgba(0,0,0,.14));background:transparent;color:inherit;border-radius:8px;padding:4px 10px;font-size:12px;cursor:pointer}',
24
+ '.fr-btn:hover{background:rgba(0,0,0,.04)}',
25
+ '.fr-btn.primary{background:var(--dsw-accent,#3b82f6);border-color:transparent;color:#fff}',
26
+ '.fr-btn.ghost{border-color:transparent;color:var(--dsw-text-weak,#6b7280)}'
27
+ ].join('\n');
28
+ function __frEnsureCss(){ if (typeof document === 'undefined' || document.getElementById(__FR_STYLE_ID)) return; var el = document.createElement('style'); el.id = __FR_STYLE_ID; el.textContent = __FR_CSS; document.head.appendChild(el); }
29
+ function __frRead(){ try { return localStorage.getItem(__FR_KEY) === 'done'; } catch (e) { return false; } }
30
+ function __frWrite(){ try { localStorage.setItem(__FR_KEY, 'done'); } catch (e) {} }
31
+ function __FirstRunCard(){
32
+ var ds = React.useState(__frRead), done = ds[0], setDone = ds[1];
33
+ var is = React.useState(0), index = is[0], setIndex = is[1];
34
+ React.useEffect(function(){ __frEnsureCss(); }, []);
35
+ if (done) return null;
36
+ var step = __FR_STEPS[index];
37
+ var last = index >= __FR_STEPS.length - 1;
38
+ var close = function(){ __frWrite(); setDone(true); };
39
+ var kids = [
40
+ React.createElement('div', { className: 'fr-head', key: 'h' }, React.createElement('span', { className: 'fr-dot' }), React.createElement('span', { className: 'fr-title' }, __FR_TITLE), React.createElement('span', { className: 'fr-count' }, (index + 1) + ' / ' + __FR_STEPS.length)),
41
+ React.createElement('div', { className: 'fr-step', key: 't' }, step.title),
42
+ React.createElement('div', { className: 'fr-body', key: 'b' }, step.body)
43
+ ];
44
+ if (step.tryIt) kids.push(React.createElement('div', { className: 'fr-try', key: 'y' }, React.createElement('span', null, '可以直接说'), React.createElement('code', null, '「' + step.tryIt + '」')));
45
+ var buttons = [React.createElement('div', { className: 'fr-progress', key: 'p' }, __FR_STEPS.map(function(_, i){ return React.createElement('span', { key: i, className: 'fr-seg' + (i <= index ? ' on' : '') }); }))];
46
+ buttons.push(React.createElement('button', { className: 'fr-btn ghost', type: 'button', onClick: close, key: 's' }, '跳过'));
47
+ if (index > 0) buttons.push(React.createElement('button', { className: 'fr-btn', type: 'button', key: 'prev', onClick: function(){ setIndex(index - 1); } }, '上一步'));
48
+ buttons.push(React.createElement('button', { className: 'fr-btn primary', type: 'button', key: 'next', onClick: function(){ if (last) close(); else setIndex(index + 1); } }, last ? '开始使用' : '下一步'));
49
+ kids.push(React.createElement('div', { className: 'fr-bar', key: 'bar' }, buttons));
50
+ return React.createElement('div', { className: 'fr-card' }, kids);
51
+ }
52
+ function __withFirstRun(Component){ return function FirstRunWrapped(props){ return React.createElement(React.Fragment, null, React.createElement(__FirstRunCard, null), React.createElement(Component, props)); }; }
53
+ // ---- end first-run guide ----
54
+
4
55
  let locale,sidebar;
5
56
  const english=()=>{try{return String(locale?.getSnapshot?.().active||'').startsWith('en')}catch{return false}};
6
57
  const t=(zh,en)=>english()?en:zh;
@@ -58,6 +109,6 @@ window.__ModuleLoader__.load({id:'dsh-code-security',factory:require=>{
58
109
  report?.hasScan?h(React.Fragment,null,h('div',{className:'secw-tools'},h('label',{htmlFor:'secw-filter'},t('问题筛选','Filter findings')),h('select',{id:'secw-filter',value:filter,onChange:e=>{setFilter(e.target.value);setLimit(50)}},h('option',{value:'all'},t('全部','All')),h('option',{value:'new'},t('仅新增','New only')),h('option',{value:'high'},t('高风险','High risk'))),h('button',{disabled:busy,onClick:()=>download('sarif')},t('下载 SARIF','Download SARIF')),h('button',{disabled:busy,onClick:()=>download('markdown')},t('下载 Markdown','Download Markdown'))),findings.length?findings.slice(0,limit).map(f=>h(FindingCard,{key:f.id,finding:f,fresh:fresh.has(f.id),onOpen:f=>location(f,false),onCopy:f=>location(f,true)})):h('p',{className:'secw-muted'},t('此筛选下没有问题。','No findings match this filter.')),findings.length>limit?h('button',{onClick:()=>setLimit(n=>n+50)},t('显示更多','Show more')):null,(report.findingsTotal??0)>200?h('p',{className:'secw-muted'},t('面板最多展示 200 项,完整结果请导出报告。','The panel shows up to 200 findings. Export the full report for all results.')):null):null):null);
59
110
  }
60
111
  const css=`.secw{max-width:1060px;min-width:0;margin:auto;font:14px/1.6 system-ui,sans-serif;color:var(--dsw-alias-label-primary,#253047)}.secw *{box-sizing:border-box}.secw h2{font-size:22px;margin:0}.secw h3{font-size:17px;margin:0 0 8px}.secw p{margin:7px 0}.secw label{display:block;margin:8px 0 4px}.secw button,.secw select,.secw input:not([type=checkbox]){font:inherit;color:inherit;border:1px solid var(--dsw-alias-border-l3,#d3dbe5);border-radius:8px;background:var(--dsw-alias-bg-layer-1,#fff);padding:8px 10px;max-width:100%}.secw input:not([type=checkbox]),.secw select{width:100%;min-width:0}.secw button{cursor:pointer;white-space:normal}.secw button:disabled{opacity:.5;cursor:default}.secw .secw-primary{background:#245ee9;color:#fff;border-color:#245ee9}.secw .secw-tools,.secw .secw-row{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:12px 0}.secw .secw-tools select{width:auto;max-width:100%}.secw .secw-muted{color:var(--dsw-alias-label-secondary,#64718a);font-size:13px}.secw .secw-card,.secw .secw-finding{border:1px solid var(--dsw-alias-border-l3,#d3dbe5);border-radius:11px;padding:14px;margin:12px 0;overflow-wrap:anywhere;background:var(--dsw-alias-bg-layer-1,#fff)}.secw .secw-finding[data-severity=critical],.secw .secw-finding[data-severity=high]{border-left:4px solid #c04836}.secw .secw-coverage{display:grid;grid-template-columns:repeat(auto-fit,minmax(200px,1fr));gap:8px;margin:10px 0}.secw .secw-engine{border:1px solid var(--dsw-alias-border-l3,#d3dbe5);border-radius:9px;padding:10px;overflow-wrap:anywhere}.secw .secw-engine strong{margin-right:8px}.secw .secw-engine small{display:block;color:var(--dsw-alias-label-secondary,#64718a)}.secw .secw-badge{display:inline-block;background:var(--dsw-alias-bg-layer-2,#f0f3f8);border-radius:5px;padding:1px 6px;font-size:12px}.secw .secw-badge[data-status=failed],.secw .secw-badge[data-status=partial]{color:#b33928}.secw .secw-error{background:#fff0ed;color:#973321;padding:10px;border-radius:8px;overflow-wrap:anywhere}.secw .secw-notice{background:#edf4ff;color:#17468c;padding:10px;border-radius:8px}.secw .secw-choice{display:flex;align-items:center;gap:8px}.secw fieldset{border:1px solid var(--dsw-alias-border-l3,#d3dbe5);border-radius:8px;margin:12px 0}.secw summary{cursor:pointer;font-weight:600}.secw pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:160px;overflow:auto;background:var(--dsw-alias-bg-layer-2,#f3f5f9);padding:10px;border-radius:6px}.secw .secw-file{font-family:ui-monospace,monospace;overflow-wrap:anywhere}.secw a{color:#245ee9}.secw :focus-visible{outline:3px solid #78a8ff;outline-offset:2px}[role=dialog]:has(.secw){width:min(1200px,calc(100vw - 48px))!important;max-width:1200px!important}@media(max-width:700px){[role=dialog]:has(.secw){width:100%!important;max-width:100%!important;flex-direction:column!important}[role=dialog]:has(.secw) nav{width:100%!important;flex-basis:auto!important;max-height:140px;overflow:auto;border-right:0}[role=dialog]:has(.secw) nav>div:has(>button){display:flex;flex-direction:row!important;flex-wrap:wrap;flex-shrink:0;gap:4px}[role=dialog]:has(.secw)>nav+div{min-width:0;width:100%}[role=dialog]:has(.secw)>nav+div>div:has(.secw){padding:12px!important;min-width:0}.secw .secw-coverage{grid-template-columns:1fr}}`;
61
- function apply(ctx){try{locale=ctx.get?.('locale')||ctx.locale}catch{locale=ctx.locale}try{sidebar=ctx.get?.('sidebarRight')||ctx.sidebarRight}catch{}ctx.slots.inject('settings.section',()=>ctx.slots.register({name:'settings.section',id:'dsh-code-security',order:59,label:()=>t('代码安全','Code security'),inject:()=>({})},Workbench))}
112
+ function apply(ctx){try{locale=ctx.get?.('locale')||ctx.locale}catch{locale=ctx.locale}try{sidebar=ctx.get?.('sidebarRight')||ctx.sidebarRight}catch{}ctx.slots.inject('settings.section',()=>ctx.slots.register({name:'settings.section',id:'dsh-code-security',order:59,label:()=>t('代码安全','Code security'),inject:()=>({})},__withFirstRun(Workbench)))}
62
113
  return{inject:['slots','locale'],apply};
63
114
  }});
package/lib/privacy.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { Finding } from './scanner.js';
2
+ export declare function isHighEntropyToken(value: string): boolean;
2
3
  export declare function evidenceFingerprint(finding: Pick<Finding, 'ruleId' | 'file' | 'snippet'> & {
3
4
  fingerprint?: string;
4
5
  }): string;
package/lib/privacy.js CHANGED
@@ -1,6 +1,19 @@
1
1
  /** Findings crossing the scanner boundary never contain complete credentials. */
2
2
  import { createHash } from 'node:crypto';
3
3
  import path from 'node:path';
4
+ export function isHighEntropyToken(value) {
5
+ if (value.length < 24 || !/\d/.test(value) || /^[0-9]+$/.test(value))
6
+ return false;
7
+ const counts = new Map();
8
+ for (const char of value)
9
+ counts.set(char, (counts.get(char) ?? 0) + 1);
10
+ let entropy = 0;
11
+ for (const count of counts.values()) {
12
+ const frequency = count / value.length;
13
+ entropy -= frequency * Math.log2(frequency);
14
+ }
15
+ return entropy >= 3.8;
16
+ }
4
17
  export function evidenceFingerprint(finding) {
5
18
  if (/^[a-f0-9]{64}$/.test(finding.fingerprint ?? ''))
6
19
  return finding.fingerprint;
@@ -15,7 +28,9 @@ export function redactText(text) {
15
28
  .replace(/(^\s*[A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD|PASS)[A-Z0-9_]*\s*=\s*)([^\r\n]+)/gm, '$1[REDACTED]')
16
29
  .replace(/\b(?:gh[pousr]_[A-Za-z0-9_]{16,}|github_pat_[A-Za-z0-9_]{20,}|sk-(?:proj-)?[A-Za-z0-9_-]{16,}|AKIA[A-Z0-9]{16})\b/g, '[REDACTED]')
17
30
  .replace(/(\bBearer\s+)[A-Za-z0-9._~+\/-]{12,}/gi, '$1[REDACTED]')
18
- .replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/g, '$1[REDACTED]@');
31
+ .replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/g, '$1[REDACTED]@')
32
+ // A credential must stay redacted when another rule reports the same source line.
33
+ .replace(/(['"`])([A-Za-z0-9+/=_-]{24,})(?:\1|$)/g, (match, quote, value) => isHighEntropyToken(value) ? quote + '[REDACTED]' + quote : match);
19
34
  }
20
35
  export function protectFinding(finding) {
21
36
  const fingerprint = evidenceFingerprint(finding);
package/lib/scanner.js CHANGED
@@ -9,7 +9,7 @@ import { createHash } from 'node:crypto';
9
9
  import { DEFAULT_EXCLUDE_DIRS } from './config.js';
10
10
  import { languageForFile, RULES } from './rules.js';
11
11
  import { policyExcludesDir, policyIgnores } from './policy.js';
12
- import { protectFinding } from './privacy.js';
12
+ import { protectFinding, isHighEntropyToken } from './privacy.js';
13
13
  import { sanitizedHtmlLines } from './syntax.js';
14
14
  const DISABLE_NEXT = /secure-review-disable-next-line(?:\s+([\w,-]+))?/;
15
15
  const DISABLE_LINE = /secure-review-disable-line(?:\s+([\w,-]+))?/;
@@ -50,31 +50,12 @@ function shouldScanFile(file) {
50
50
  return true;
51
51
  return TEXT_EXTENSIONS.test(file);
52
52
  }
53
- /** Shannon 熵;只对 24 位以上且以字母数字为主的 token 判分。 */
54
- function shannon(text) {
55
- if (text.length < 16)
56
- return 0;
57
- const counts = new Map();
58
- for (const char of text)
59
- counts.set(char, (counts.get(char) ?? 0) + 1);
60
- let entropy = 0;
61
- for (const count of counts.values()) {
62
- const p = count / text.length;
63
- entropy -= p * Math.log2(p);
64
- }
65
- return entropy;
66
- }
67
53
  const TOKEN_PATTERN = /['"]([A-Za-z0-9+/=_-]{24,})['"]/g;
68
- const HIGH_ENTROPY_THRESHOLD = 3.8;
69
54
  function detectHighEntropy(line, lineNumber, rel, rule) {
70
55
  const findings = [];
71
56
  for (const match of line.matchAll(TOKEN_PATTERN)) {
72
57
  const token = match[1];
73
- if (/^[0-9]+$/.test(token))
74
- continue;
75
- if (!/\d/.test(token))
76
- continue;
77
- if (shannon(token) < HIGH_ENTROPY_THRESHOLD)
58
+ if (!isHighEntropyToken(token))
78
59
  continue;
79
60
  const id = makeId(rule.id, rel, lineNumber, token.slice(0, 32));
80
61
  findings.push({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-code-security",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "对代码与 Git 改动进行本地静态安全检查,并追踪修复结果。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -35,7 +35,9 @@
35
35
  "patch": "./cordis.patch.yml"
36
36
  },
37
37
  "client": {
38
- "inject": ["@deepseek-ai/dsh-client-ui-settings"],
38
+ "inject": [
39
+ "@deepseek-ai/dsh-client-ui-settings"
40
+ ],
39
41
  "platform": "web"
40
42
  }
41
43
  },