gitnexus 1.6.8-rc.43 → 1.6.8-rc.45
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/dist/server/api.d.ts +17 -0
- package/dist/server/api.js +61 -5
- package/dist/server/git-clone.d.ts +41 -1
- package/dist/server/git-clone.js +197 -11
- package/package.json +1 -1
- package/web/assets/{agent-lSrfPH05.js → agent-D5J40fBb.js} +103 -103
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-DxJw65fT.js +1 -0
- package/web/assets/architectureDiagram-UL44E2DR-CUVrwu-f.js +36 -0
- package/web/assets/{blockDiagram-7IZFK4PR-B4niSLvR.js → blockDiagram-7IZFK4PR-BkWhpzWB.js} +1 -1
- package/web/assets/{c4Diagram-Y2BXMSZH-DIzEXalR.js → c4Diagram-Y2BXMSZH-DtBvpzWD.js} +1 -1
- package/web/assets/{chunk-3SSMPTDK-BzGeeheI.js → chunk-3SSMPTDK-B4_etUhr.js} +2 -2
- package/web/assets/{chunk-5IMINLNL-CnTLsXIV.js → chunk-5IMINLNL-DOdTeQri.js} +1 -1
- package/web/assets/{chunk-5VCL7Z4A-w1BCx98Z.js → chunk-5VCL7Z4A-CQ-2dYWj.js} +1 -1
- package/web/assets/chunk-6764PJDD-DpIS7hxD.js +1 -0
- package/web/assets/{chunk-67TQ5CYL-CChC2cAv.js → chunk-67TQ5CYL-4sNPJU1q.js} +3 -3
- package/web/assets/{chunk-7W6UQGC5-CgFyfZ6k.js → chunk-7W6UQGC5-KyEG0HQg.js} +1 -1
- package/web/assets/{chunk-AZZRMDJM-5E9ARdDQ.js → chunk-AZZRMDJM-y-xJBuMz.js} +1 -1
- package/web/assets/{chunk-INKRHTLW-ELpfuk9h.js → chunk-INKRHTLW-Nu4ri9P-.js} +1 -1
- package/web/assets/{chunk-JQRUD6KW-Dcq_Qnyy.js → chunk-JQRUD6KW-Dbva2Z17.js} +1 -1
- package/web/assets/{chunk-KGFNY3KK-W-VR57xb.js → chunk-KGFNY3KK-JGMJvKCK.js} +1 -1
- package/web/assets/chunk-KGYTTC2M-CWC_c3H9.js +161 -0
- package/web/assets/{chunk-KNLZD3CH-BuZBOcso.js → chunk-KNLZD3CH-BGlP6X9R.js} +1 -1
- package/web/assets/{chunk-KRXBNO2N-DH2NPM7I.js → chunk-KRXBNO2N-BW5o0KUN.js} +1 -1
- package/web/assets/{chunk-LCXTWHL2-03IqIAdZ.js → chunk-LCXTWHL2-DYctxkWH.js} +1 -1
- package/web/assets/{chunk-LII3EMHJ-3X33tCpU.js → chunk-LII3EMHJ-D-jm-dLa.js} +1 -1
- package/web/assets/{chunk-QA3QBVWF-D5Ha3GgB.js → chunk-QA3QBVWF-BgRW3SXd.js} +1 -1
- package/web/assets/{chunk-RG4AUYOV-DLFjj1rt.js → chunk-RG4AUYOV-Dai0blvV.js} +1 -1
- package/web/assets/{chunk-T2UQINTJ-kGYG7ppP.js → chunk-T2UQINTJ-DSBqkzH7.js} +1 -1
- package/web/assets/chunk-T5OCTHI4-DdZvN-9i.js +1 -0
- package/web/assets/{chunk-UY5QBCOK-Du-c56gs.js → chunk-UY5QBCOK-DyF0vyGd.js} +1 -1
- package/web/assets/{chunk-W44A43WB-lrCpuupA.js → chunk-W44A43WB-JqMLIpR0.js} +1 -1
- package/web/assets/{chunk-ZXARS5L4-CSZRiyOf.js → chunk-ZXARS5L4-CxN8oiwI.js} +1 -1
- package/web/assets/classDiagram-KGZ6W3CR-DGMIeRUV.js +1 -0
- package/web/assets/classDiagram-v2-72OJOZXJ-DGMIeRUV.js +1 -0
- package/web/assets/{cose-bilkent-UX7MHV2Q-BHv2bbtl.js → cose-bilkent-UX7MHV2Q-D71wNYRJ.js} +1 -1
- package/web/assets/dagre-ND4H6XIP-DjIPJ0yu.js +4 -0
- package/web/assets/diagram-3NCE3AQN-D-gExQR8.js +43 -0
- package/web/assets/diagram-GF46GFSD-B7qp9EBf.js +24 -0
- package/web/assets/{diagram-HNR7UZ2L-fUWreZzx.js → diagram-HNR7UZ2L-DFCfd5LI.js} +2 -2
- package/web/assets/diagram-QXG6HAR7-CVD497Ig.js +24 -0
- package/web/assets/diagram-WEQXMOUZ-BZgvAFQK.js +10 -0
- package/web/assets/{erDiagram-L5TCEMPS-DqEAcwC_.js → erDiagram-L5TCEMPS-eFDAIcoP.js} +1 -1
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-BQm9QzEa.js +1 -0
- package/web/assets/{flowDiagram-H6V6AXG4-BUEpeW9m.js → flowDiagram-H6V6AXG4-mlFvvBcJ.js} +3 -3
- package/web/assets/{ganttDiagram-JCBTUEKG-BHQssTz6.js → ganttDiagram-JCBTUEKG-D3iL2Aet.js} +1 -1
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-Cr_Bm2Nb.js +1 -0
- package/web/assets/gitGraphDiagram-S2ZK5IYY-CPIDgxGm.js +106 -0
- package/web/assets/index-80yfnWPU.js +635 -0
- package/web/assets/{index-DeuHlyzm.css → index-_lgn7hs5.css} +1 -1
- package/web/assets/info-J43DQDTF-KCYPFFUO-Cs4p2oyk.js +1 -0
- package/web/assets/infoDiagram-3YFTVSEB-A6e-DQS2.js +2 -0
- package/web/assets/{ishikawaDiagram-BNXS4ZKH-XxO6eDu4.js → ishikawaDiagram-BNXS4ZKH-CmzHpns0.js} +1 -1
- package/web/assets/{journeyDiagram-M6C3CM3L-DNPYCpvq.js → journeyDiagram-M6C3CM3L-CvArszLo.js} +1 -1
- package/web/assets/{kanban-definition-75IXJCU3-DeSVq5eO.js → kanban-definition-75IXJCU3-BRr8hm0l.js} +3 -3
- package/web/assets/{katex-K3KEBU37-CrVLz8l7.js → katex-K3KEBU37-CTc5BslQ.js} +1 -1
- package/web/assets/{mindmap-definition-2TDM6QVE-CgEl9e61.js → mindmap-definition-2TDM6QVE-BmRh5T1l.js} +1 -1
- package/web/assets/packet-YPE3B663-LP52Z2RK-CKCrztD2.js +1 -0
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-BB7pHqoD.js +1 -0
- package/web/assets/{pieDiagram-CU6KROY3-E3RA4hXq.js → pieDiagram-CU6KROY3-j4wKlimb.js} +1 -1
- package/web/assets/{quadrantDiagram-VICAPDV7-bAm6Hi_5.js → quadrantDiagram-VICAPDV7-yXSZ2lLz.js} +1 -1
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-DhoTOcuK.js +1 -0
- package/web/assets/{requirementDiagram-JXO7QTGE-B70bTqxy.js → requirementDiagram-JXO7QTGE-bMkROmfn.js} +1 -1
- package/web/assets/{sankeyDiagram-URQDO5SZ-LFJz53u5.js → sankeyDiagram-URQDO5SZ-1qEjlFIB.js} +1 -1
- package/web/assets/{sequenceDiagram-VS2MUI6T-CLt7nidU.js → sequenceDiagram-VS2MUI6T-Dws3nv7I.js} +3 -3
- package/web/assets/stateDiagram-7D4R322I-DePQE9Ss.js +1 -0
- package/web/assets/stateDiagram-v2-36443NZ5-XwJow906.js +1 -0
- package/web/assets/{timeline-definition-O6YCAMPW-8DBKNdJr.js → timeline-definition-O6YCAMPW-D8q1B9gj.js} +1 -1
- package/web/assets/treeView-BLDUP644-QA4HXRO3-ODehyGKL.js +1 -0
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-BfpgC-7o.js +1 -0
- package/web/assets/{vennDiagram-MWXL3ELB-DhqnfFx6.js → vennDiagram-MWXL3ELB-C1wh9rL3.js} +3 -3
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-CNeL6VHE.js +1 -0
- package/web/assets/{wardleyDiagram-CUQ6CDDI-DcliVPWw.js → wardleyDiagram-CUQ6CDDI-D4Y6EyeO.js} +1 -1
- package/web/assets/{xychartDiagram-N2JHSOCM-CYmW296g.js → xychartDiagram-N2JHSOCM-CGE9TFhV.js} +1 -1
- package/web/index.html +13 -15
- package/web/assets/architecture-7EHR7CIX-6QZW5X65-BETj2x4g.js +0 -1
- package/web/assets/architectureDiagram-UL44E2DR-CgHRMZJ8.js +0 -36
- package/web/assets/chunk-2T2R6R2M-n6s9JZqv.js +0 -4
- package/web/assets/chunk-2UTLFMKG-CMBB1TMN.js +0 -1
- package/web/assets/chunk-4R4BOZG6-mKhL59ul.js +0 -159
- package/web/assets/chunk-6764PJDD-ChwMM2z4.js +0 -1
- package/web/assets/chunk-7J6CGLKN-DT-b53FT.js +0 -10
- package/web/assets/chunk-C62D2QBJ-CDAWj26E.js +0 -1
- package/web/assets/chunk-CEXFNPSA-D68Tk6ls.js +0 -1
- package/web/assets/chunk-CilyBKbf.js +0 -1
- package/web/assets/chunk-J5EP6P6S-DxWW0yvu.js +0 -1
- package/web/assets/chunk-KGYTTC2M-C6PHeuay.js +0 -1
- package/web/assets/chunk-RERM46MO-DQNbXtfw.js +0 -1
- package/web/assets/chunk-RKZBBQEN-BmTPLSyv.js +0 -1
- package/web/assets/chunk-RLI5ZMPA-DExu2DOK.js +0 -1
- package/web/assets/chunk-T5OCTHI4--9wWpVws.js +0 -1
- package/web/assets/chunk-UP6H54XL-DsKdC6jC.js +0 -1
- package/web/assets/chunk-UXSXWOXI-DR81EqLr.js +0 -1
- package/web/assets/classDiagram-KGZ6W3CR-BTr7xsR8.js +0 -1
- package/web/assets/classDiagram-v2-72OJOZXJ-CakV709X.js +0 -1
- package/web/assets/dagre-ND4H6XIP-DsIr9MOv.js +0 -4
- package/web/assets/diagram-3NCE3AQN-BW6iSZhw.js +0 -43
- package/web/assets/diagram-GF46GFSD-5IsY7kfK.js +0 -24
- package/web/assets/diagram-QXG6HAR7-Cia81gsm.js +0 -24
- package/web/assets/diagram-WEQXMOUZ-CLfT1sdo.js +0 -10
- package/web/assets/eventmodeling-FCH6USID-MREXMVOE-B3myHUst.js +0 -1
- package/web/assets/gitGraph-WXDBUCRP-R675I2BI-DF3rtf4A.js +0 -1
- package/web/assets/gitGraphDiagram-S2ZK5IYY-CCwVLT__.js +0 -106
- package/web/assets/index-64YMDMOE.js +0 -626
- package/web/assets/info-J43DQDTF-KCYPFFUO-R4XV_akJ.js +0 -1
- package/web/assets/infoDiagram-3YFTVSEB-hHbd_QCD.js +0 -2
- package/web/assets/packet-YPE3B663-LP52Z2RK-CgQPdNZa.js +0 -1
- package/web/assets/pie-LRSECV5Y-TCRJHUBD-BzWOwc_c.js +0 -1
- package/web/assets/radar-GUYGQ44K-RDLRG3WG-B7KG0FN7.js +0 -1
- package/web/assets/stateDiagram-7D4R322I-DfdeOa9A.js +0 -1
- package/web/assets/stateDiagram-v2-36443NZ5-C-c52-8M.js +0 -1
- package/web/assets/treeView-BLDUP644-QA4HXRO3-CRSxEwLO.js +0 -1
- package/web/assets/treemap-LRROVOQU-LLAWBHMP-a0PfKoVn.js +0 -1
- package/web/assets/wardley-L42UT6IY-5TKZOOLJ-BKQ6e5mC.js +0 -1
- /package/web/assets/{chunk-AQ6EADP3-CCFje6lL.js → chunk-AQ6EADP3-CZhslHi-.js} +0 -0
- /package/web/assets/{context-builder-C6wAZwss.js → context-builder-BHiFUA8O.js} +0 -0
package/dist/server/api.d.ts
CHANGED
|
@@ -72,5 +72,22 @@ export declare const handleFileRequest: (req: {
|
|
|
72
72
|
export declare const handleQueryRequest: (req: express.Request, res: express.Response, resolveRepo: (repoName?: string) => Promise<{
|
|
73
73
|
storagePath: string;
|
|
74
74
|
} | undefined>) => Promise<void>;
|
|
75
|
+
/**
|
|
76
|
+
* Validate the optional `token` field of POST /api/analyze. Returns an
|
|
77
|
+
* { status, error } to send, or null when the token is absent or valid.
|
|
78
|
+
*
|
|
79
|
+
* The token is a GitHub PAT: charset-restricted (blocks CRLF header
|
|
80
|
+
* smuggling), length-bounded (1–256), and bound to github.com using the SAME
|
|
81
|
+
* GITHUB_TOKEN_HOSTS allowlist + hostname parse as resolveGitCredential, so a
|
|
82
|
+
* token the API accepts is exactly the one buildGitEnv will inject — and one
|
|
83
|
+
* it rejects is never sent off github.com.
|
|
84
|
+
*
|
|
85
|
+
* Exported for unit tests (the route validation is otherwise only reachable
|
|
86
|
+
* by booting the server).
|
|
87
|
+
*/
|
|
88
|
+
export declare function validateAnalyzeToken(repoToken: unknown, repoUrl: unknown): {
|
|
89
|
+
status: number;
|
|
90
|
+
error: string;
|
|
91
|
+
} | null;
|
|
75
92
|
export declare const createServer: (port: number, host?: string) => Promise<void>;
|
|
76
93
|
export {};
|
package/dist/server/api.js
CHANGED
|
@@ -23,7 +23,7 @@ import { mountMCPEndpoints } from './mcp-http.js';
|
|
|
23
23
|
import { fileURLToPath } from 'url';
|
|
24
24
|
import { JobManager } from './analyze-job.js';
|
|
25
25
|
import { assertString, escapeRegExp, BadRequestError, createRouteLimiter } from './validation.js';
|
|
26
|
-
import { extractRepoName, getCloneDir, cloneOrPull } from './git-clone.js';
|
|
26
|
+
import { extractRepoName, getCloneDir, cloneOrPull, warnIfInsecureAzureConfig, GITHUB_TOKEN_HOSTS, } from './git-clone.js';
|
|
27
27
|
import { createAnalyzeUploadHandler } from './analyze-upload.js';
|
|
28
28
|
import { createLocalhostOriginGuard, normalizeBoundHost } from './middleware.js';
|
|
29
29
|
import { createLaunchAnalysisWorker } from './analyze-launch.js';
|
|
@@ -580,7 +580,45 @@ export const handleQueryRequest = async (req, res, resolveRepo) => {
|
|
|
580
580
|
res.status(500).json({ error: err.message || 'Query failed' });
|
|
581
581
|
}
|
|
582
582
|
};
|
|
583
|
+
/**
|
|
584
|
+
* Validate the optional `token` field of POST /api/analyze. Returns an
|
|
585
|
+
* { status, error } to send, or null when the token is absent or valid.
|
|
586
|
+
*
|
|
587
|
+
* The token is a GitHub PAT: charset-restricted (blocks CRLF header
|
|
588
|
+
* smuggling), length-bounded (1–256), and bound to github.com using the SAME
|
|
589
|
+
* GITHUB_TOKEN_HOSTS allowlist + hostname parse as resolveGitCredential, so a
|
|
590
|
+
* token the API accepts is exactly the one buildGitEnv will inject — and one
|
|
591
|
+
* it rejects is never sent off github.com.
|
|
592
|
+
*
|
|
593
|
+
* Exported for unit tests (the route validation is otherwise only reachable
|
|
594
|
+
* by booting the server).
|
|
595
|
+
*/
|
|
596
|
+
export function validateAnalyzeToken(repoToken, repoUrl) {
|
|
597
|
+
if (repoToken === undefined)
|
|
598
|
+
return null;
|
|
599
|
+
if (typeof repoToken !== 'string')
|
|
600
|
+
return { status: 400, error: '"token" must be a string' };
|
|
601
|
+
if (repoToken.length === 0 || repoToken.length > 256)
|
|
602
|
+
return { status: 400, error: '"token" length must be between 1 and 256' };
|
|
603
|
+
if (!/^[A-Za-z0-9._~+/=-]+$/.test(repoToken))
|
|
604
|
+
return { status: 400, error: '"token" contains invalid characters' };
|
|
605
|
+
if (!repoUrl || typeof repoUrl !== 'string')
|
|
606
|
+
return { status: 400, error: '"token" requires "url"' };
|
|
607
|
+
let tokenHost;
|
|
608
|
+
try {
|
|
609
|
+
tokenHost = new URL(repoUrl).hostname.toLowerCase();
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
return { status: 400, error: '"url" must be a valid URL when "token" is provided' };
|
|
613
|
+
}
|
|
614
|
+
if (!GITHUB_TOKEN_HOSTS.has(tokenHost))
|
|
615
|
+
return { status: 400, error: '"token" is only supported for github.com URLs' };
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
583
618
|
export const createServer = async (port, host = '127.0.0.1') => {
|
|
619
|
+
// Surface a cleartext Azure DevOps PAT config at boot (operators rarely
|
|
620
|
+
// read per-request logs). Warn-only — http:// self-hosted stays supported.
|
|
621
|
+
warnIfInsecureAzureConfig();
|
|
584
622
|
const app = express();
|
|
585
623
|
app.disable('x-powered-by');
|
|
586
624
|
// Trust X-Forwarded-* headers only when the connection comes from the
|
|
@@ -1272,7 +1310,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1272
1310
|
// POST /api/analyze — start a new analysis job
|
|
1273
1311
|
app.post('/api/analyze', createRouteLimiter({ limit: 10 }), requireLocalhostOrigin, async (req, res) => {
|
|
1274
1312
|
try {
|
|
1275
|
-
const { url: repoUrl, path: repoLocalPath, force, embeddings, dropEmbeddings } = req.body;
|
|
1313
|
+
const { url: repoUrl, path: repoLocalPath, force, embeddings, dropEmbeddings, token: repoToken, } = req.body;
|
|
1276
1314
|
// Input type validation
|
|
1277
1315
|
if (repoUrl !== undefined && typeof repoUrl !== 'string') {
|
|
1278
1316
|
res.status(400).json({ error: '"url" must be a string' });
|
|
@@ -1286,6 +1324,13 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1286
1324
|
res.status(400).json({ error: 'Provide "url" (git URL) or "path" (local path)' });
|
|
1287
1325
|
return;
|
|
1288
1326
|
}
|
|
1327
|
+
// Token: optional, restricted charset to prevent header smuggling
|
|
1328
|
+
// (CRLF), bound length, and bound to github.com (see validateAnalyzeToken).
|
|
1329
|
+
const tokenError = validateAnalyzeToken(repoToken, repoUrl);
|
|
1330
|
+
if (tokenError) {
|
|
1331
|
+
res.status(tokenError.status).json({ error: tokenError.error });
|
|
1332
|
+
return;
|
|
1333
|
+
}
|
|
1289
1334
|
// Path validation. The previous `normalize !== resolve` guard was inert
|
|
1290
1335
|
// (both collapse `..` identically) and only false-rejected trailing
|
|
1291
1336
|
// slashes, so it is dropped. Analyzing a local path the operator names
|
|
@@ -1302,9 +1347,20 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1302
1347
|
return;
|
|
1303
1348
|
}
|
|
1304
1349
|
const job = jobManager.createJob({ repoUrl, repoPath: repoLocalPath });
|
|
1305
|
-
// If job was already running (dedup), just return its id
|
|
1350
|
+
// If job was already running (dedup), just return its id. The token is
|
|
1351
|
+
// not part of the dedup identity and is never stored on the job, so a
|
|
1352
|
+
// token on THIS request had no effect — the existing job already
|
|
1353
|
+
// cloned (or is cloning) with whatever credentials its originating
|
|
1354
|
+
// request supplied. Surface `tokenIgnored` so an authenticated caller
|
|
1355
|
+
// isn't misled into thinking their PAT took effect on a reused job.
|
|
1306
1356
|
if (job.status !== 'queued') {
|
|
1307
|
-
|
|
1357
|
+
const body = {
|
|
1358
|
+
jobId: job.id,
|
|
1359
|
+
status: job.status,
|
|
1360
|
+
};
|
|
1361
|
+
if (repoToken !== undefined)
|
|
1362
|
+
body.tokenIgnored = true;
|
|
1363
|
+
res.status(202).json(body);
|
|
1308
1364
|
return;
|
|
1309
1365
|
}
|
|
1310
1366
|
// Mark as active synchronously to prevent race with concurrent requests
|
|
@@ -1326,7 +1382,7 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1326
1382
|
jobManager.updateJob(job.id, {
|
|
1327
1383
|
progress: { phase: progress.phase, percent: 5, message: progress.message },
|
|
1328
1384
|
});
|
|
1329
|
-
});
|
|
1385
|
+
}, repoToken ? { token: repoToken } : undefined);
|
|
1330
1386
|
}
|
|
1331
1387
|
if (!targetPath) {
|
|
1332
1388
|
throw new Error('No target path resolved');
|
|
@@ -35,6 +35,25 @@ export interface CloneProgress {
|
|
|
35
35
|
*
|
|
36
36
|
* Exported so the separator placement is testable without mocking spawn.
|
|
37
37
|
*/
|
|
38
|
+
/**
|
|
39
|
+
* Detect Azure DevOps URLs — both self-hosted (via AZURE_DEVOPS_URL env)
|
|
40
|
+
* and cloud (dev.azure.com / *.visualstudio.com).
|
|
41
|
+
*
|
|
42
|
+
* Self-hosted Azure DevOps Server instances use arbitrary hostnames
|
|
43
|
+
* (e.g. `http://tfs.corp.example/Collection/Project/_git/Repo`), so the
|
|
44
|
+
* function checks `AZURE_DEVOPS_URL` first. Cloud addresses are a
|
|
45
|
+
* hardcoded fallback so PAT injection works out-of-the-box for
|
|
46
|
+
* dev.azure.com without extra configuration.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isAzureDevOpsUrl(url: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* One-time startup warning when AZURE_DEVOPS_URL is configured over cleartext
|
|
51
|
+
* http:// — the Azure DevOps PAT would then be sent unencrypted on every
|
|
52
|
+
* clone. Self-hosted instances that only serve http are still supported (we
|
|
53
|
+
* do not refuse), but operators rarely read request-time logs, so surface it
|
|
54
|
+
* at boot too. Call once from server startup.
|
|
55
|
+
*/
|
|
56
|
+
export declare function warnIfInsecureAzureConfig(): void;
|
|
38
57
|
export declare function buildCloneArgs(url: string, targetDir: string): string[];
|
|
39
58
|
/**
|
|
40
59
|
* Normalize a git URL into a comparable form.
|
|
@@ -97,4 +116,25 @@ export declare function assertRemoteMatchesRequestedUrl(targetDir: string, reque
|
|
|
97
116
|
* `--` (e.g. `--upload-pack=evil`) cannot be interpreted as a git option
|
|
98
117
|
* (CodeQL js/second-order-command-line-injection).
|
|
99
118
|
*/
|
|
100
|
-
export declare function cloneOrPull(url: string, targetDir: string, onProgress?: (progress: CloneProgress) => void
|
|
119
|
+
export declare function cloneOrPull(url: string, targetDir: string, onProgress?: (progress: CloneProgress) => void, options?: {
|
|
120
|
+
token?: string;
|
|
121
|
+
}): Promise<string>;
|
|
122
|
+
/**
|
|
123
|
+
* Hosts the per-request GitHub PAT may be sent to. Exported so the
|
|
124
|
+
* /api/analyze boundary check and this injection-site check share one
|
|
125
|
+
* allowlist (they must agree, or a token accepted by the API could be
|
|
126
|
+
* silently dropped — or worse — at injection).
|
|
127
|
+
*/
|
|
128
|
+
export declare const GITHUB_TOKEN_HOSTS: ReadonlySet<string>;
|
|
129
|
+
/**
|
|
130
|
+
* Build the spawn env for `git`. Suppresses credential prompts and, when a
|
|
131
|
+
* credential resolves (see resolveGitCredential), injects a single
|
|
132
|
+
* host-scoped Authorization header via the `GIT_CONFIG_*` env protocol
|
|
133
|
+
* (git ≥2.31) so credentials never appear in argv or the URL. Appends after
|
|
134
|
+
* any existing `GIT_CONFIG_COUNT` rather than overwriting it. Exported for
|
|
135
|
+
* unit tests.
|
|
136
|
+
*/
|
|
137
|
+
export declare function buildGitEnv(baseEnv: NodeJS.ProcessEnv, options?: {
|
|
138
|
+
token?: string;
|
|
139
|
+
url?: string;
|
|
140
|
+
}): NodeJS.ProcessEnv;
|
package/dist/server/git-clone.js
CHANGED
|
@@ -197,6 +197,60 @@ function assertNotPrivateIPv4(ip) {
|
|
|
197
197
|
*
|
|
198
198
|
* Exported so the separator placement is testable without mocking spawn.
|
|
199
199
|
*/
|
|
200
|
+
/**
|
|
201
|
+
* Detect Azure DevOps URLs — both self-hosted (via AZURE_DEVOPS_URL env)
|
|
202
|
+
* and cloud (dev.azure.com / *.visualstudio.com).
|
|
203
|
+
*
|
|
204
|
+
* Self-hosted Azure DevOps Server instances use arbitrary hostnames
|
|
205
|
+
* (e.g. `http://tfs.corp.example/Collection/Project/_git/Repo`), so the
|
|
206
|
+
* function checks `AZURE_DEVOPS_URL` first. Cloud addresses are a
|
|
207
|
+
* hardcoded fallback so PAT injection works out-of-the-box for
|
|
208
|
+
* dev.azure.com without extra configuration.
|
|
209
|
+
*/
|
|
210
|
+
export function isAzureDevOpsUrl(url) {
|
|
211
|
+
try {
|
|
212
|
+
// Strip a single trailing dot: `dev.azure.com.` is a valid absolute FQDN
|
|
213
|
+
// that resolves to the same host, so it must match too.
|
|
214
|
+
const host = new URL(url).hostname.toLowerCase().replace(/\.$/, '');
|
|
215
|
+
// Self-hosted: match against the configured base URL.
|
|
216
|
+
const configuredBase = process.env.AZURE_DEVOPS_URL;
|
|
217
|
+
if (configuredBase) {
|
|
218
|
+
try {
|
|
219
|
+
const baseHost = new URL(configuredBase).hostname.toLowerCase().replace(/\.$/, '');
|
|
220
|
+
if (host === baseHost)
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
/* invalid AZURE_DEVOPS_URL — fall through to cloud check */
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Cloud fallback.
|
|
228
|
+
return host === 'dev.azure.com' || host.endsWith('.visualstudio.com');
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* One-time startup warning when AZURE_DEVOPS_URL is configured over cleartext
|
|
236
|
+
* http:// — the Azure DevOps PAT would then be sent unencrypted on every
|
|
237
|
+
* clone. Self-hosted instances that only serve http are still supported (we
|
|
238
|
+
* do not refuse), but operators rarely read request-time logs, so surface it
|
|
239
|
+
* at boot too. Call once from server startup.
|
|
240
|
+
*/
|
|
241
|
+
export function warnIfInsecureAzureConfig() {
|
|
242
|
+
const base = process.env.AZURE_DEVOPS_URL;
|
|
243
|
+
if (!base)
|
|
244
|
+
return;
|
|
245
|
+
try {
|
|
246
|
+
if (new URL(base).protocol === 'http:') {
|
|
247
|
+
logger.warn('AZURE_DEVOPS_URL is configured over cleartext http:// — the Azure DevOps PAT will be sent unencrypted. Prefer https:// where your instance supports it.');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
/* invalid AZURE_DEVOPS_URL — isAzureDevOpsUrl already tolerates this */
|
|
252
|
+
}
|
|
253
|
+
}
|
|
200
254
|
export function buildCloneArgs(url, targetDir) {
|
|
201
255
|
return ['clone', '--depth', '1', '--', url, targetDir];
|
|
202
256
|
}
|
|
@@ -327,7 +381,7 @@ export async function assertRemoteMatchesRequestedUrl(targetDir, requestedUrl) {
|
|
|
327
381
|
* `--` (e.g. `--upload-pack=evil`) cannot be interpreted as a git option
|
|
328
382
|
* (CodeQL js/second-order-command-line-injection).
|
|
329
383
|
*/
|
|
330
|
-
export async function cloneOrPull(url, targetDir, onProgress) {
|
|
384
|
+
export async function cloneOrPull(url, targetDir, onProgress, options) {
|
|
331
385
|
// Containment barrier — inline with the canonical path.relative idiom so
|
|
332
386
|
// CodeQL recognizes the sanitizer at every following filesystem and
|
|
333
387
|
// subprocess sink. The same `safeTarget` is used for every downstream
|
|
@@ -355,28 +409,160 @@ export async function cloneOrPull(url, targetDir, onProgress) {
|
|
|
355
409
|
// whatever remote the dir was originally cloned from.
|
|
356
410
|
await assertRemoteMatchesRequestedUrl(safeTarget, url);
|
|
357
411
|
onProgress?.({ phase: 'pulling', message: 'Pulling latest changes...' });
|
|
358
|
-
await runGit(['pull', '--ff-only'], safeTarget);
|
|
412
|
+
await runGit(['pull', '--ff-only'], safeTarget, { token: options?.token, url });
|
|
359
413
|
}
|
|
360
414
|
else {
|
|
361
415
|
await fs.mkdir(path.dirname(safeTarget), { recursive: true });
|
|
362
416
|
onProgress?.({ phase: 'cloning', message: `Cloning ${url}...` });
|
|
363
|
-
await runGit(buildCloneArgs(url, safeTarget));
|
|
417
|
+
await runGit(buildCloneArgs(url, safeTarget), undefined, { token: options?.token, url });
|
|
364
418
|
}
|
|
365
419
|
return safeTarget;
|
|
366
420
|
}
|
|
367
|
-
|
|
421
|
+
/**
|
|
422
|
+
* Hosts the per-request GitHub PAT may be sent to. Exported so the
|
|
423
|
+
* /api/analyze boundary check and this injection-site check share one
|
|
424
|
+
* allowlist (they must agree, or a token accepted by the API could be
|
|
425
|
+
* silently dropped — or worse — at injection).
|
|
426
|
+
*/
|
|
427
|
+
export const GITHUB_TOKEN_HOSTS = new Set(['github.com', 'www.github.com']);
|
|
428
|
+
/**
|
|
429
|
+
* Resolve at most ONE git credential for a clone/pull, by server-side policy
|
|
430
|
+
* keyed on the clone host against a fixed allowlist (never a free-form user
|
|
431
|
+
* toggle):
|
|
432
|
+
* 1. a per-request GitHub PAT — only for hosts in GITHUB_TOKEN_HOSTS;
|
|
433
|
+
* 2. else the server's AZURE_DEVOPS_PAT — only for Azure DevOps hosts;
|
|
434
|
+
* 3. else none.
|
|
435
|
+
* The two host sets are disjoint, so at most one credential ever applies; the
|
|
436
|
+
* GitHub token taking precedence is deterministic for the pathological case
|
|
437
|
+
* where AZURE_DEVOPS_URL is itself configured to a github.com host. Returns
|
|
438
|
+
* the base64 of the Basic-auth `user:secret` pair, or undefined.
|
|
439
|
+
*
|
|
440
|
+
* Security note (re CodeQL js/user-controlled-bypass): the clone URL is
|
|
441
|
+
* user-controlled and selects WHICH credential applies, but it cannot
|
|
442
|
+
* redirect a credential to an arbitrary host — the host is matched against
|
|
443
|
+
* fixed server-side allowlists (GITHUB_TOKEN_HOSTS, isAzureDevOpsUrl's
|
|
444
|
+
* dev.azure.com/*.visualstudio.com/configured AZURE_DEVOPS_URL), and the
|
|
445
|
+
* emitted header is host-scoped (buildExtraHeaderKey). A URL outside the
|
|
446
|
+
* allowlists yields no credential. The selection is therefore server-policy,
|
|
447
|
+
* not a bypass the user can steer.
|
|
448
|
+
*/
|
|
449
|
+
function resolveGitCredential(options) {
|
|
450
|
+
const url = options?.url;
|
|
451
|
+
if (!url)
|
|
452
|
+
return undefined;
|
|
453
|
+
let host;
|
|
454
|
+
try {
|
|
455
|
+
host = new URL(url).hostname.toLowerCase();
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
return undefined;
|
|
459
|
+
}
|
|
460
|
+
// 1. Per-request GitHub PAT — github.com only (mirrors the /api/analyze
|
|
461
|
+
// host-bind so the user's token is never sent off github.com).
|
|
462
|
+
if (options.token && GITHUB_TOKEN_HOSTS.has(host)) {
|
|
463
|
+
return Buffer.from(`x-access-token:${options.token}`).toString('base64');
|
|
464
|
+
}
|
|
465
|
+
// 2. Server-configured Azure DevOps PAT — Azure hosts only.
|
|
466
|
+
const azurePat = process.env.AZURE_DEVOPS_PAT;
|
|
467
|
+
if (azurePat && isAzureDevOpsUrl(url)) {
|
|
468
|
+
return Buffer.from(`:${azurePat}`).toString('base64');
|
|
469
|
+
}
|
|
470
|
+
return undefined;
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Build the host-scoped git config key `http.<origin+path>.extraHeader` from
|
|
474
|
+
* the raw clone URL, so the Authorization header is attached only to the
|
|
475
|
+
* intended origin (and its clone sub-requests like /info/refs), never a
|
|
476
|
+
* redirect target. Derived from the SAME raw URL git clones from — not the
|
|
477
|
+
* normalize-for-compare form, which strips `.git` and would desync the key
|
|
478
|
+
* from the wire URL and silently disable the header. Userinfo/query/fragment
|
|
479
|
+
* are dropped (not part of git's URL match) and control characters stripped
|
|
480
|
+
* (git rejects a newline in a config key outright).
|
|
481
|
+
*/
|
|
482
|
+
function buildExtraHeaderKey(url) {
|
|
483
|
+
let scoped;
|
|
484
|
+
try {
|
|
485
|
+
const u = new URL(url);
|
|
486
|
+
u.username = '';
|
|
487
|
+
u.password = '';
|
|
488
|
+
u.search = '';
|
|
489
|
+
u.hash = '';
|
|
490
|
+
scoped = `${u.protocol}//${u.host}${u.pathname}`;
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
scoped = scoped.replace(/[\r\n\0]/g, '');
|
|
496
|
+
return `http.${scoped}.extraHeader`;
|
|
497
|
+
}
|
|
498
|
+
/**
|
|
499
|
+
* Warn (do not block) when a credential is about to be sent over cleartext
|
|
500
|
+
* http://. Base64 is encoding, not encryption, so an on-path observer can
|
|
501
|
+
* read the PAT. We keep http:// working for self-hosted Azure DevOps Server.
|
|
502
|
+
*/
|
|
503
|
+
function warnIfCleartextCredential(url) {
|
|
504
|
+
if (!url)
|
|
505
|
+
return;
|
|
506
|
+
try {
|
|
507
|
+
const u = new URL(url);
|
|
508
|
+
if (u.protocol === 'http:') {
|
|
509
|
+
logger.warn(`Sending a git credential over cleartext http:// (${u.host}) — base64 is not encryption. Prefer https:// where the host supports it.`);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
catch {
|
|
513
|
+
/* resolver already validated the URL */
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Build the spawn env for `git`. Suppresses credential prompts and, when a
|
|
518
|
+
* credential resolves (see resolveGitCredential), injects a single
|
|
519
|
+
* host-scoped Authorization header via the `GIT_CONFIG_*` env protocol
|
|
520
|
+
* (git ≥2.31) so credentials never appear in argv or the URL. Appends after
|
|
521
|
+
* any existing `GIT_CONFIG_COUNT` rather than overwriting it. Exported for
|
|
522
|
+
* unit tests.
|
|
523
|
+
*/
|
|
524
|
+
export function buildGitEnv(baseEnv, options) {
|
|
525
|
+
const env = {
|
|
526
|
+
...baseEnv,
|
|
527
|
+
// Prevent git from prompting for credentials (hangs the process)
|
|
528
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
529
|
+
// Ensure no credential helper tries to open a GUI prompt
|
|
530
|
+
GIT_ASKPASS: process.platform === 'win32' ? 'echo' : '/bin/true',
|
|
531
|
+
// Scrub git's HTTP/transport trace vars: if inherited from the parent
|
|
532
|
+
// process they dump every request header — including the injected
|
|
533
|
+
// Authorization header — to stderr, which runGit captures and logs.
|
|
534
|
+
// `undefined` makes child_process omit the key from the child env.
|
|
535
|
+
GIT_TRACE: undefined,
|
|
536
|
+
GIT_TRACE_CURL: undefined,
|
|
537
|
+
GIT_TRACE_PACKET: undefined,
|
|
538
|
+
GIT_CURL_VERBOSE: undefined,
|
|
539
|
+
};
|
|
540
|
+
const credential = resolveGitCredential(options);
|
|
541
|
+
const key = options?.url ? buildExtraHeaderKey(options.url) : undefined;
|
|
542
|
+
if (credential && key) {
|
|
543
|
+
// Append after any GIT_CONFIG_* the operator already set, so we never
|
|
544
|
+
// clobber their git config (e.g. an enforced http.sslVerify).
|
|
545
|
+
const existing = Number.parseInt(env.GIT_CONFIG_COUNT ?? '', 10);
|
|
546
|
+
const base = Number.isInteger(existing) && existing > 0 ? existing : 0;
|
|
547
|
+
env.GIT_CONFIG_COUNT = String(base + 1);
|
|
548
|
+
env[`GIT_CONFIG_KEY_${base}`] = key;
|
|
549
|
+
env[`GIT_CONFIG_VALUE_${base}`] = `Authorization: Basic ${credential}`;
|
|
550
|
+
warnIfCleartextCredential(options?.url);
|
|
551
|
+
}
|
|
552
|
+
return env;
|
|
553
|
+
}
|
|
554
|
+
// `options` carries the inputs the credential resolver needs: a per-request
|
|
555
|
+
// GitHub `token` and the clone `url`. buildGitEnv injects at most ONE
|
|
556
|
+
// host-scoped Authorization header (GitHub PAT for github.com, else the
|
|
557
|
+
// server's AZURE_DEVOPS_PAT for Azure hosts) via the GIT_CONFIG_* protocol —
|
|
558
|
+
// never in argv. See resolveGitCredential / buildExtraHeaderKey.
|
|
559
|
+
function runGit(args, cwd, options) {
|
|
368
560
|
return new Promise((resolve, reject) => {
|
|
369
561
|
const proc = spawn('git', args, {
|
|
370
562
|
cwd,
|
|
371
563
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
372
564
|
windowsHide: true,
|
|
373
|
-
env:
|
|
374
|
-
...process.env,
|
|
375
|
-
// Prevent git from prompting for credentials (hangs the process)
|
|
376
|
-
GIT_TERMINAL_PROMPT: '0',
|
|
377
|
-
// Ensure no credential helper tries to open a GUI prompt
|
|
378
|
-
GIT_ASKPASS: process.platform === 'win32' ? 'echo' : '/bin/true',
|
|
379
|
-
},
|
|
565
|
+
env: buildGitEnv(process.env, options),
|
|
380
566
|
});
|
|
381
567
|
let stderr = '';
|
|
382
568
|
proc.stderr.on('data', (chunk) => {
|
package/package.json
CHANGED