xtra-cli 1.0.3 → 1.0.5
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/bin/xtra.js +3 -137
- package/dist/commands/access.js +1 -184
- package/dist/commands/admin.js +5 -118
- package/dist/commands/audit.js +1 -67
- package/dist/commands/branch.js +1 -212
- package/dist/commands/checkout.js +1 -80
- package/dist/commands/ci.js +31 -341
- package/dist/commands/completion.js +85 -224
- package/dist/commands/diff.js +6 -162
- package/dist/commands/doctor.js +2 -164
- package/dist/commands/env.js +2 -70
- package/dist/commands/export.js +2 -83
- package/dist/commands/generate.js +3 -179
- package/dist/commands/history.js +4 -77
- package/dist/commands/import.js +1 -121
- package/dist/commands/init.js +7 -205
- package/dist/commands/integration.js +11 -188
- package/dist/commands/jit-run.js +5 -0
- package/dist/commands/local.js +16 -198
- package/dist/commands/login.js +16 -198
- package/dist/commands/login.test.js +16 -51
- package/dist/commands/logout.js +1 -0
- package/dist/commands/logs.js +3 -121
- package/dist/commands/profile.js +14 -184
- package/dist/commands/project.js +1 -165
- package/dist/commands/rollback.js +1 -95
- package/dist/commands/rotate.js +3 -93
- package/dist/commands/run.js +11 -215
- package/dist/commands/scan.js +25 -127
- package/dist/commands/secrets.js +14 -305
- package/dist/commands/simulate.js +6 -109
- package/dist/commands/status.js +2 -93
- package/dist/commands/template.js +22 -276
- package/dist/commands/ui.js +2 -289
- package/dist/commands/watch.js +17 -123
- package/dist/lib/api.js +1 -187
- package/dist/lib/api.test.js +1 -89
- package/dist/lib/audit.js +1 -136
- package/dist/lib/config.js +1 -72
- package/dist/lib/config.test.js +1 -47
- package/dist/lib/crypto.js +1 -50
- package/dist/lib/manifest.js +1 -52
- package/dist/lib/profiles.js +1 -103
- package/package.json +7 -2
package/dist/commands/run.js
CHANGED
|
@@ -1,215 +1,11 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
}));
|
|
13
|
-
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
-
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
-
}) : function(o, v) {
|
|
16
|
-
o["default"] = v;
|
|
17
|
-
});
|
|
18
|
-
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
-
var ownKeys = function(o) {
|
|
20
|
-
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
-
var ar = [];
|
|
22
|
-
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
-
return ar;
|
|
24
|
-
};
|
|
25
|
-
return ownKeys(o);
|
|
26
|
-
};
|
|
27
|
-
return function (mod) {
|
|
28
|
-
if (mod && mod.__esModule) return mod;
|
|
29
|
-
var result = {};
|
|
30
|
-
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
-
__setModuleDefault(result, mod);
|
|
32
|
-
return result;
|
|
33
|
-
};
|
|
34
|
-
})();
|
|
35
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
-
};
|
|
38
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.runCommand = void 0;
|
|
40
|
-
const commander_1 = require("commander");
|
|
41
|
-
const api_1 = require("../lib/api");
|
|
42
|
-
const child_process_1 = require("child_process");
|
|
43
|
-
const chalk_1 = __importDefault(require("chalk"));
|
|
44
|
-
const ora_1 = __importDefault(require("ora"));
|
|
45
|
-
const config_1 = require("../lib/config");
|
|
46
|
-
const crypto_1 = require("../lib/crypto");
|
|
47
|
-
const fs = __importStar(require("fs"));
|
|
48
|
-
const path = __importStar(require("path"));
|
|
49
|
-
const dotenv_1 = __importDefault(require("dotenv"));
|
|
50
|
-
exports.runCommand = new commander_1.Command("run")
|
|
51
|
-
.description("Run a command with injected secrets")
|
|
52
|
-
.option("-p, --project <projectId>", "Project ID")
|
|
53
|
-
.option("-e, --env <environment>", "Environment (development, staging, production)", "development")
|
|
54
|
-
.option("-b, --branch <branchName>", "Branch Name")
|
|
55
|
-
.option("--shell", "Enable shell mode (needed for npm run, shell built-ins on Windows)", false)
|
|
56
|
-
.argument("<command>", "Command to run")
|
|
57
|
-
.argument("[args...]", "Command arguments")
|
|
58
|
-
.addHelpText("after", `
|
|
59
|
-
Examples:
|
|
60
|
-
$ xtra run npm start
|
|
61
|
-
$ xtra run -e production -- npm run build
|
|
62
|
-
$ xtra run -p proj_123 -b feature-branch -- python script.py
|
|
63
|
-
$ xtra run --shell "echo $SECRET_KEY"
|
|
64
|
-
`)
|
|
65
|
-
.action(async (command, args, options) => {
|
|
66
|
-
// console.log("Run Options:", options);
|
|
67
|
-
let { project, env, branch, shell: useShell } = options;
|
|
68
|
-
// Load .xtrarc from CWD first (project-local config), then fall back to global conf store
|
|
69
|
-
const rc = (0, config_1.getRcConfig)();
|
|
70
|
-
// Use active branch from config if not specified
|
|
71
|
-
if (!branch)
|
|
72
|
-
branch = rc.branch;
|
|
73
|
-
// Normalize Env
|
|
74
|
-
const envMap = { dev: "development", stg: "staging", prod: "production" };
|
|
75
|
-
env = envMap[env] || env || rc.env;
|
|
76
|
-
if (!project)
|
|
77
|
-
project = rc.project;
|
|
78
|
-
if (!project) {
|
|
79
|
-
console.error(chalk_1.default.red("Error: Project ID is required. Use -p <id> or checkout a branch."));
|
|
80
|
-
process.exit(1);
|
|
81
|
-
}
|
|
82
|
-
const spinner = (0, ora_1.default)(`Fetching secrets for ${env} (branch: ${branch})...`).start();
|
|
83
|
-
let secrets = null;
|
|
84
|
-
// ── Local mode override ──────────────────────────────────────────────────
|
|
85
|
-
// If XTRA_LOCAL_MODE=true or localMode config flag is set,
|
|
86
|
-
// read from .env.local instead of calling the API.
|
|
87
|
-
const isLocal = (0, config_1.getConfigValue)("localMode") === true
|
|
88
|
-
|| process.env.XTRA_LOCAL_MODE === "true";
|
|
89
|
-
if (isLocal) {
|
|
90
|
-
const localFile = path.resolve(process.cwd(), ".env.local");
|
|
91
|
-
if (!fs.existsSync(localFile)) {
|
|
92
|
-
spinner.fail("Local mode is ON but .env.local not found. Run 'xtra local sync' first.");
|
|
93
|
-
process.exit(1);
|
|
94
|
-
}
|
|
95
|
-
const parsed = dotenv_1.default.parse(fs.readFileSync(localFile, "utf8"));
|
|
96
|
-
secrets = parsed;
|
|
97
|
-
spinner.succeed(chalk_1.default.yellow(`🔌 Local mode: loaded ${Object.keys(secrets).length} secrets from .env.local`));
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
try {
|
|
101
|
-
// 1. Fetch Secrets
|
|
102
|
-
secrets = await api_1.api.getSecrets(project, env, branch);
|
|
103
|
-
if (!secrets || Object.keys(secrets).length === 0) {
|
|
104
|
-
spinner.warn(chalk_1.default.yellow("No secrets found for this environment."));
|
|
105
|
-
}
|
|
106
|
-
else {
|
|
107
|
-
spinner.succeed(`Loaded ${Object.keys(secrets).length} secrets (Online).`);
|
|
108
|
-
// Update manifest
|
|
109
|
-
try {
|
|
110
|
-
const { hash } = require("../lib/crypto");
|
|
111
|
-
const { updateManifest } = require("../lib/manifest");
|
|
112
|
-
updateManifest(project, env, secrets, hash);
|
|
113
|
-
}
|
|
114
|
-
catch (mErr) {
|
|
115
|
-
// Manifest update failed
|
|
116
|
-
}
|
|
117
|
-
// Cache secrets locally
|
|
118
|
-
try {
|
|
119
|
-
const cacheKey = `cache.${project}.${env}.${branch}`;
|
|
120
|
-
const encrypted = (0, crypto_1.encrypt)(JSON.stringify(secrets));
|
|
121
|
-
(0, config_1.setConfig)(cacheKey, encrypted);
|
|
122
|
-
// console.log(chalk.gray("Secrets cached successfully."));
|
|
123
|
-
}
|
|
124
|
-
catch (cacheErr) {
|
|
125
|
-
console.error(chalk_1.default.yellow("Warning: Failed to cache secrets."));
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
catch (error) {
|
|
130
|
-
// Offline fallback
|
|
131
|
-
const cacheKey = `cache.${project}.${env}.${branch}`;
|
|
132
|
-
const cachedData = (0, config_1.getConfigValue)(cacheKey);
|
|
133
|
-
if (cachedData) {
|
|
134
|
-
try {
|
|
135
|
-
const decrypted = (0, crypto_1.decrypt)(cachedData);
|
|
136
|
-
secrets = JSON.parse(decrypted);
|
|
137
|
-
spinner.warn(chalk_1.default.yellow(`Offline Mode: Loaded ${Object.keys(secrets).length} secrets from cache.`));
|
|
138
|
-
}
|
|
139
|
-
catch (decryptErr) {
|
|
140
|
-
spinner.fail("Failed to fetch secrets and cache is corrupt.");
|
|
141
|
-
process.exit(1);
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
else {
|
|
145
|
-
spinner.fail("Failed to fetch secrets and no local cache available.");
|
|
146
|
-
if (error.response) {
|
|
147
|
-
if (error.response.status === 404) {
|
|
148
|
-
console.error(chalk_1.default.red(`\nError: Project '${project}' not found or you don't have access.`));
|
|
149
|
-
}
|
|
150
|
-
else if (error.response.status === 401) {
|
|
151
|
-
console.error(chalk_1.default.red(`\nError: Unauthorized. Please run 'xtra login' again.`));
|
|
152
|
-
}
|
|
153
|
-
else {
|
|
154
|
-
console.error(chalk_1.default.red(`\nError: ${error.response.data.error || error.message}`));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
else {
|
|
158
|
-
console.error(chalk_1.default.red(`\nError: ${error.message}`));
|
|
159
|
-
}
|
|
160
|
-
process.exit(1);
|
|
161
|
-
}
|
|
162
|
-
} // end try/catch (cloud mode)
|
|
163
|
-
} // end else (cloud mode)
|
|
164
|
-
// Log Audit (Always)
|
|
165
|
-
try {
|
|
166
|
-
const { logAudit } = require("../lib/audit");
|
|
167
|
-
// Only log if we have secrets
|
|
168
|
-
if (secrets && Object.keys(secrets).length > 0) {
|
|
169
|
-
logAudit("SECRET_ACCESS", project, env, { method: "run", keys: Object.keys(secrets), branch });
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch (e) {
|
|
173
|
-
console.error("Audit Error:", e);
|
|
174
|
-
}
|
|
175
|
-
// 2. Prepare Environment
|
|
176
|
-
const envVars = {
|
|
177
|
-
...process.env,
|
|
178
|
-
...secrets, // Overwrite local env with injected secrets
|
|
179
|
-
};
|
|
180
|
-
// 3. Spawn Child Process — shell: false by default to prevent command injection
|
|
181
|
-
// When --shell is passed (e.g. for npm run start on Windows), allow shell mode
|
|
182
|
-
const SHELL_UNSAFE = /[;&|`$<>\\\n]/g;
|
|
183
|
-
if (!useShell && SHELL_UNSAFE.test(command)) {
|
|
184
|
-
console.error(chalk_1.default.red("Error: Command contains unsafe characters. Use --shell if intentional."));
|
|
185
|
-
process.exit(1);
|
|
186
|
-
}
|
|
187
|
-
// Production confirmation gate
|
|
188
|
-
if (env === "production") {
|
|
189
|
-
const inquirer = require("inquirer");
|
|
190
|
-
const { confirm } = await inquirer.prompt([{
|
|
191
|
-
type: "confirm",
|
|
192
|
-
name: "confirm",
|
|
193
|
-
message: chalk_1.default.red(`⚠ You are about to run a command in PRODUCTION with ${Object.keys(secrets || {}).length} injected secrets. Continue?`),
|
|
194
|
-
default: false,
|
|
195
|
-
}]);
|
|
196
|
-
if (!confirm) {
|
|
197
|
-
console.log(chalk_1.default.yellow("Aborted."));
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
console.log(chalk_1.default.gray(`> ${command} ${args.join(" ")}`));
|
|
202
|
-
const isWindows = process.platform === "win32";
|
|
203
|
-
const child = (0, child_process_1.spawn)(command, args, {
|
|
204
|
-
env: envVars,
|
|
205
|
-
stdio: "inherit",
|
|
206
|
-
shell: useShell || isWindows, // Auto-enable shell on Windows for better compatibility (e.g. npm, etc)
|
|
207
|
-
});
|
|
208
|
-
child.on("exit", (code) => {
|
|
209
|
-
process.exit(code ?? 0);
|
|
210
|
-
});
|
|
211
|
-
child.on("error", (err) => {
|
|
212
|
-
console.error(chalk_1.default.red(`Failed to start command: ${err.message}`));
|
|
213
|
-
process.exit(1);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
1
|
+
var Ie=Object.create;var O=Object.defineProperty;var ke=Object.getOwnPropertyDescriptor;var Ae=Object.getOwnPropertyNames;var Ee=Object.getPrototypeOf,Ce=Object.prototype.hasOwnProperty;var $=(e,t)=>()=>(e&&(t=e(e=0)),t);var b=(e,t)=>{for(var s in t)O(e,s,{get:t[s],enumerable:!0})},Q=(e,t,s,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Ae(t))!Ce.call(e,n)&&n!==s&&O(e,n,{get:()=>t[n],enumerable:!(r=ke(t,n))||r.enumerable});return e};var p=(e,t,s)=>(s=e!=null?Ie(Ee(e)):{},Q(t||!e||!e.__esModule?O(s,"default",{value:e,enumerable:!0}):s,e)),I=e=>Q(O({},"__esModule",{value:!0}),e);var ee={};b(ee,{clearConfig:()=>Oe,deleteConfig:()=>Re,getAuthToken:()=>q,getBreakGlassSession:()=>Le,getConfig:()=>h,getConfigValue:()=>R,getProjectConfig:()=>Te,getRcConfig:()=>K,setBreakGlassSession:()=>Ne,setConfig:()=>J});var Z,y,G,B,g,M,h,R,J,Oe,Re,q,Le,Ne,Te,K,k=$(()=>{Z=p(require("conf")),y=p(require("fs")),G=p(require("path")),B="https://www.xtrasecurity.in/api",g=new Z.default({projectName:"xtra-cli",defaults:{apiUrl:B}}),M=g.get("apiUrl");M&&!process.env.XTRA_API_URL&&(M.includes("localhost")||M.includes("xtra-security.vercel.app"))&&g.set("apiUrl",B);h=()=>({...g.store,apiUrl:process.env.XTRA_API_URL||g.get("apiUrl")||B}),R=e=>g.get(e),J=(e,t)=>{if(g.set(e,t),e==="token"&&process.platform!=="win32"&&y.default.existsSync(g.path))try{y.default.chmodSync(g.path,384)}catch{}},Oe=()=>g.clear(),Re=e=>g.delete(e),q=()=>g.get("token"),Le=()=>g.get("breakGlassSessionId"),Ne=e=>{e?g.set("breakGlassSessionId",e):g.delete("breakGlassSessionId")},Te=async()=>{try{let e=G.default.join(process.cwd(),"xtra.json");if(y.default.existsSync(e)){let t=y.default.readFileSync(e,"utf-8");return JSON.parse(t)}return null}catch{return null}},K=()=>{let e={};try{let t=G.default.join(process.cwd(),".xtrarc");y.default.existsSync(t)&&(e=JSON.parse(y.default.readFileSync(t,"utf-8")))}catch{}return{project:e.project||g.get("project")||"",env:e.env||g.get("env")||"development",branch:e.branch||g.get("branch")||"main"}}});var te,se,re,Ue,o,L,H=$(()=>{te=p(require("axios"));k();se=p(require("fs")),re=p(require("path")),Ue=JSON.parse(se.readFileSync(re.join(__dirname,"../../package.json"),"utf-8")),o=()=>{let{apiUrl:e}=h(),t=q();return te.default.create({baseURL:e,headers:{"Content-Type":"application/json","X-CLI-Version":Ue.version,...t?{Authorization:`Bearer ${t}`}:{}}})},L={login:async(e,t,s)=>(await o().post("/auth/cli-login",{email:e,password:t,apiKey:s})).data,getSecrets:async(e,t,s="main")=>(await o().get(`/projects/${e}/envs/${t}/secrets?branch=${s}`)).data,getSecretVersions:async(e,t,s="main")=>(await o().get(`/projects/${e}/envs/${t}/secrets?includeVersions=true&branch=${s}`)).data,setSecrets:async(e,t,s,r,n="main")=>(await o().post(`/projects/${e}/envs/${t}/secrets`,{secrets:s,expectedVersions:r,branch:n})).data,getSecretDetails:async(e,t,s,r="main")=>(await o().get(`/projects/${e}/envs/${t}/secrets/${s}?branch=${r}`)).data,syncLogs:async e=>(await o().post("/audit/cli-logs",{logs:e})).data,linkSecret:async(e,t,s,r,n,a)=>(await o().post(`/projects/${e}/envs/${t}/secrets/link`,{key:s,sourceProjectId:r,sourceEnv:n,sourceKey:a})).data,rotateSecret:async(e,t,s,r,n)=>(await o().post(`/projects/${e}/envs/${t}/secrets/${s}/rotate`,{strategy:r,parsedNewValue:n})).data,promoteSecret:async(e,t,s)=>(await o().post(`/projects/${e}/envs/${t}/secrets/${s}/promote`,{})).data,verifyAuditLogs:async()=>(await o().get("/audit/verify")).data,exportAuditLogs:async(e,t,s,r)=>{let n={format:e};return t&&(n.startDate=t),s&&(n.endDate=s),r&&(n.projectId=r),(await o().post("/audit/export",n,{responseType:e==="csv"?"text":"json"})).data},requestAccess:async(e,t,s,r)=>(await o().post("/access/request",{projectId:e,secretId:r,reason:t,duration:s})).data,approveAccess:async(e,t)=>(await o().post("/access/approve",{requestId:e,decision:t})).data,listAccessRequests:async e=>{let{workspace:t}=h(),s=t?`&workspaceId=${t}`:"";return(await o().get(`/access/list?mode=${e}${s}`)).data},getAccessRequestStatus:async e=>(await o().get(`/access/request?id=${e}`)).data,getProjects:async()=>{let{workspace:e}=h(),t=e?`/project?workspaceId=${e}`:"/project";return(await o().get(t)).data},getBranches:async e=>(await o().get(`/branch?projectId=${e}`)).data,createBranch:async(e,t,s)=>(await o().post("/branch",{projectId:e,name:t,description:s})).data,deleteBranch:async e=>(await o().delete(`/branch/${e}`)).data,updateBranch:async(e,t)=>(await o().put("/branch",{id:e,...t})).data,getRoles:async()=>(await o().get("/admin/roles")).data,getUsers:async e=>{let t=e?`?teamId=${e}`:"";return(await o().get(`/admin/users${t}`)).data},setUserRole:async(e,t,s)=>(await o().put("/admin/users/role",{email:e,role:t,teamId:s})).data,getIntegrationStatus:async e=>(await o().get(`/integrations/${e}`)).data,getIntegrationRepos:async e=>(await o().get(`/integrations/${e}/sync`)).data.repos,syncSecretsToGithub:async e=>(await o().post("/integrations/github/sync",e)).data,exportKubernetesSecret:async(e,t)=>{let s=new URLSearchParams(t).toString();return(await o().get(`/projects/${e}/kubernetes?${s}`)).data},getSecretHistory:async(e,t,s)=>(await o().get(`/projects/${e}/envs/${t}/secrets/${s}/history`)).data,rollbackSecret:async(e,t,s,r)=>(await o().post(`/projects/${e}/envs/${t}/secrets/${s}/history`,{version:r})).data,cloneEnvironment:async(e,t,s,r,n)=>(await o().post(`/projects/${e}/envs/clone`,{fromEnv:t,toEnv:s,overwrite:r,branch:n})).data,generateJitLink:async e=>(await o().post("/jit/generate",e)).data,claimJitLink:async e=>(await o().post("/jit/claim",{token:e})).data,getJitInfo:async e=>(await o().get(`/jit/${e}`)).data,post:async(e,t)=>(await o().post(e,t)).data}});var pe={};b(pe,{decrypt:()=>E,encrypt:()=>A,hash:()=>De});function ce(){let e=ne.get("masterKey");e||(e=S.default.randomBytes(32).toString("hex"),ne.set("masterKey",e));let t=(0,ie.machineIdSync)();return S.default.createHash("sha256").update(e+t).digest()}var S,oe,ie,ae,ne,A,E,De,N=$(()=>{S=p(require("crypto")),oe=p(require("conf")),ie=require("node-machine-id"),ae="aes-256-gcm",ne=new oe.default({projectName:"xtra-cli-crypto"});A=e=>{let t=S.default.randomBytes(16),s=ce(),r=S.default.createCipheriv(ae,s,t),n=r.update(e,"utf8","hex");n+=r.final("hex");let a=r.getAuthTag();return{iv:t.toString("hex"),content:n,tag:a.toString("hex")}},E=e=>{let t=ce(),s=Buffer.from(e.iv,"hex"),r=Buffer.from(e.tag,"hex"),n=S.default.createDecipheriv(ae,t,s);n.setAuthTag(r);let a=n.update(e.content,"hex","utf8");return a+=n.final("utf8"),a},De=e=>S.default.createHash("sha256").update(e).digest("hex")});var le={};b(le,{MANIFEST_DIR:()=>T,MANIFEST_FILE:()=>U,loadManifest:()=>ge,saveManifest:()=>de,updateManifest:()=>Fe});function ge(){if(!w.default.existsSync(U))return null;try{let e=w.default.readFileSync(U,"utf-8");return JSON.parse(e)}catch{return null}}function de(e){w.default.existsSync(T)||w.default.mkdirSync(T,{recursive:!0}),w.default.writeFileSync(U,JSON.stringify(e,null,2),"utf-8")}function Fe(e,t,s,r){let n=ge()||{projectId:e,environment:t,lastSyncedAt:new Date().toISOString(),secrets:{}};n.projectId=e,n.environment=t,n.lastSyncedAt=new Date().toISOString(),Object.entries(s).forEach(([a,m])=>{let u=r(m);n.secrets[a]={hash:u,updatedAt:new Date().toISOString()}}),de(n)}var w,V,T,U,ue=$(()=>{w=p(require("fs")),V=p(require("path")),T=V.default.join(process.cwd(),".xtra"),U=V.default.join(T,"manifest.json")});var ye={};b(ye,{loadAuditLogs:()=>D,logAudit:()=>Pe,saveAuditLogs:()=>F,syncAuditLogs:()=>Me});function _e(e){if(!e||typeof e!="object")return e;let t={...e},s=["password","token","secret","value","apiKey","secretValue"];for(let r of Object.keys(t))s.some(n=>r.toLowerCase().includes(n))&&(t[r]="[REDACTED]");return t}async function me(e){try{if(!h().token)return!1;let s={action:e.action,entity:"cli-project",entityId:e.projectId||"global",details:_e(e.details),timestamp:e.timestamp,projectId:e.projectId};return await L.post("/audit/cli-logs",{logs:[s]}),!0}catch(t){return process.env.DEBUG&&console.error("Failed to send audit log to backend:",t),!1}}function Pe(e,t,s,r){let n={id:(0,fe.v4)(),timestamp:new Date().toISOString(),action:e,projectId:t,environment:s,details:r,synced:!1},a=D();a.push(n),F(a),me(n).then(m=>{if(m){n.synced=!0;let u=D(),P=u.findIndex(l=>l.id===n.id);P!==-1&&(u[P].synced=!0,F(u))}}).catch(()=>{})}function D(){if(!j.default.existsSync(X))return[];try{let e=j.default.readFileSync(X,"utf-8"),t=JSON.parse(e),s=E(t);return JSON.parse(s)}catch{return[]}}function F(e){j.default.existsSync(z)||j.default.mkdirSync(z,{recursive:!0});let t=JSON.stringify(e),s=A(t);j.default.writeFileSync(X,JSON.stringify(s),"utf-8")}async function Me(){let e=D(),t=e.filter(r=>!r.synced);if(t.length===0)return 0;let s=0;for(let r of t)await me(r)&&(r.synced=!0,s++);return s>0&&F(e),s}var j,W,fe,z,X,he=$(()=>{j=p(require("fs")),W=p(require("path")),fe=require("uuid");N();k();H();z=W.default.join(process.cwd(),".xtra"),X=W.default.join(z,"audit.enc")});var Je={};b(Je,{runCommand:()=>Be});module.exports=I(Je);var Se=require("commander");H();var ve=require("child_process"),d=p(require("chalk")),xe=p(require("ora"));k();N();var _=p(require("fs")),we=p(require("path")),je=p(require("dotenv")),Be=new Se.Command("run").description("Run a command with injected secrets").option("-p, --project <projectId>","Project ID").option("-e, --env <environment>","Environment (development, staging, production)","development").option("-b, --branch <branchName>","Branch Name").option("--shell","Enable shell mode (needed for npm run, shell built-ins on Windows)",!1).argument("<command>","Command to run").argument("[args...]","Command arguments").addHelpText("after",`
|
|
2
|
+
Examples:
|
|
3
|
+
$ xtra run npm start
|
|
4
|
+
$ xtra run -e production -- npm run build
|
|
5
|
+
$ xtra run -p proj_123 -b feature-branch -- python script.py
|
|
6
|
+
$ xtra run --shell "echo $SECRET_KEY"
|
|
7
|
+
`).action(async(e,t,s)=>{let{project:r,env:n,branch:a,shell:m}=s,u=K();a||(a=u.branch),n={dev:"development",stg:"staging",prod:"production"}[n]||n||u.env,r||(r=u.project),r||(console.error(d.default.red("Error: Project ID is required. Use -p <id> or checkout a branch.")),process.exit(1));let l=(0,xe.default)(`Fetching secrets for ${n} (branch: ${a})...`).start(),c=null;if(R("localMode")===!0||process.env.XTRA_LOCAL_MODE==="true"){let i=we.resolve(process.cwd(),".env.local");_.existsSync(i)||(l.fail("Local mode is ON but .env.local not found. Run 'xtra local sync' first."),process.exit(1)),c=je.default.parse(_.readFileSync(i,"utf8")),l.succeed(d.default.yellow(`\u{1F50C} Local mode: loaded ${Object.keys(c).length} secrets from .env.local`))}else try{if(c=await L.getSecrets(r,n,a),!c||Object.keys(c).length===0)l.warn(d.default.yellow("No secrets found for this environment."));else{l.succeed(`Loaded ${Object.keys(c).length} secrets (Online).`);try{let{hash:i}=(N(),I(pe)),{updateManifest:f}=(ue(),I(le));f(r,n,c,i)}catch{}try{let i=`cache.${r}.${n}.${a}`,f=A(JSON.stringify(c));J(i,f)}catch{console.error(d.default.yellow("Warning: Failed to cache secrets."))}}}catch(i){let f=`cache.${r}.${n}.${a}`;if(i.response&&[401,403,404].includes(i.response.status)){try{let{deleteConfig:x}=(k(),I(ee));x&&x(f)}catch{}l.fail("Access Denied."),i.response.status===404?console.error(d.default.red(`
|
|
8
|
+
Error: Project '${r}' not found or your access has been revoked.`)):i.response.status===401?console.error(d.default.red(`
|
|
9
|
+
Error: Unauthorized. Please run 'xtra login' again.`)):console.error(d.default.red(`
|
|
10
|
+
Error: ${i.response.data.error||"You do not have permission to access these secrets."}`)),process.exit(1)}let Y=R(f);if(Y)try{let x=E(Y);c=JSON.parse(x),l.warn(d.default.yellow(`Offline Mode: Loaded ${Object.keys(c).length} secrets from cache.`))}catch{l.fail("Failed to fetch secrets and cache is corrupt."),process.exit(1)}else l.fail("Failed to fetch secrets and no local cache available."),console.error(d.default.dim(`
|
|
11
|
+
Details: ${i.message}`)),process.exit(1)}try{let{logAudit:i}=(he(),I(ye));c&&Object.keys(c).length>0&&i("SECRET_ACCESS",r,n,{method:"run",keys:Object.keys(c),branch:a})}catch(i){console.error("Audit Error:",i)}let $e={...process.env,...c};if(!m&&/[;&|`$<>\\\n]/g.test(e)&&(console.error(d.default.red("Error: Command contains unsafe characters. Use --shell if intentional.")),process.exit(1)),n==="production"){let i=require("inquirer"),{confirm:f}=await i.prompt([{type:"confirm",name:"confirm",message:d.default.red(`\u26A0 You are about to run a command in PRODUCTION with ${Object.keys(c||{}).length} injected secrets. Continue?`),default:!1}]);if(!f){console.log(d.default.yellow("Aborted."));return}}console.log(d.default.gray(`> ${e} ${t.join(" ")}`));let be=process.platform==="win32",v=(0,ve.spawn)(e,t,{env:$e,stdio:"inherit",shell:m||be}),C=()=>{if(c)for(let i in c)c[i]="**********"};process.on("SIGINT",()=>{C(),v&&v.kill("SIGINT"),process.exit(0)}),process.on("SIGTERM",()=>{C(),v&&v.kill("SIGTERM"),process.exit(0)}),v.on("exit",i=>{C(),process.exit(i??0)}),v.on("error",i=>{console.error(d.default.red(`Failed to start command: ${i.message}`)),C(),process.exit(1)})});0&&(module.exports={runCommand});
|
package/dist/commands/scan.js
CHANGED
|
@@ -1,127 +1,25 @@
|
|
|
1
|
-
"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// 1. Check .env tracking
|
|
27
|
-
if (isEnvFileTracked()) {
|
|
28
|
-
console.error(chalk_1.default.red("CRITICAL: .env file is tracked by git!"));
|
|
29
|
-
issues++;
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
// console.log(chalk.green("✔ .env file is safe (not tracked)."));
|
|
33
|
-
}
|
|
34
|
-
// 2. Scan Files
|
|
35
|
-
const filesToScan = staged ? getStagedFiles() : getAllFiles();
|
|
36
|
-
// Pattern: xs_ (Looking for API keys or similar tokens if we had a specific format)
|
|
37
|
-
// Also naive check for "password", "secret", "key" = "..."
|
|
38
|
-
const patterns = [
|
|
39
|
-
{ name: "Xtra API Key", regex: /xs_[a-zA-Z0-9]{10,}/ },
|
|
40
|
-
// Add more patterns here
|
|
41
|
-
];
|
|
42
|
-
filesToScan.forEach(file => {
|
|
43
|
-
try {
|
|
44
|
-
// Skip binary or large files?
|
|
45
|
-
if (!fs_1.default.existsSync(file))
|
|
46
|
-
return;
|
|
47
|
-
const content = fs_1.default.readFileSync(file, "utf-8");
|
|
48
|
-
patterns.forEach(p => {
|
|
49
|
-
if (p.regex.test(content)) {
|
|
50
|
-
console.error(chalk_1.default.red(`[${file}] Potential Secret Detected: ${p.name}`));
|
|
51
|
-
issues++;
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
catch (e) {
|
|
56
|
-
// Ignore (e.g. dir or binary)
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
if (issues > 0) {
|
|
60
|
-
console.error(chalk_1.default.red(`\n✖ Scan failed. found ${issues} issues.`));
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
else {
|
|
64
|
-
console.log(chalk_1.default.green("\n✔ Scan passed. No secrets found."));
|
|
65
|
-
}
|
|
66
|
-
});
|
|
67
|
-
function installGitHook() {
|
|
68
|
-
const hookDir = path_1.default.join(process.cwd(), ".git", "hooks");
|
|
69
|
-
const hookPath = path_1.default.join(hookDir, "pre-commit");
|
|
70
|
-
if (!fs_1.default.existsSync(hookDir)) {
|
|
71
|
-
console.error(chalk_1.default.red("Error: .git directory not found. Is this a git repository?"));
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
const hookContent = `#!/bin/sh
|
|
75
|
-
# XtraSync Secret Scan
|
|
76
|
-
echo "Running XtraSync Secret Scan..."
|
|
77
|
-
|
|
78
|
-
# Check if 'xtra' is globally installed or use local script
|
|
79
|
-
if command -v xtra >/dev/null 2>&1; then
|
|
80
|
-
xtra scan --staged
|
|
81
|
-
elif [ -f "./node_modules/.bin/xtra" ]; then
|
|
82
|
-
./node_modules/.bin/xtra scan --staged
|
|
83
|
-
else
|
|
84
|
-
# Using the binary directly if running in dev environment (unlikely in prod use case)
|
|
85
|
-
# For dev testing:
|
|
86
|
-
if [ -f "./dist/bin/xtra.js" ]; then
|
|
87
|
-
node ./dist/bin/xtra.js scan --staged
|
|
88
|
-
else
|
|
89
|
-
echo "Warning: XtraSync CLI not found. Skipping scan."
|
|
90
|
-
fi
|
|
91
|
-
fi
|
|
92
|
-
|
|
93
|
-
# Exit code of scan is passed through
|
|
94
|
-
`;
|
|
95
|
-
fs_1.default.writeFileSync(hookPath, hookContent, { mode: 0o755 });
|
|
96
|
-
console.log(chalk_1.default.green("✔ Pre-commit hook installed successfully."));
|
|
97
|
-
}
|
|
98
|
-
function isEnvFileTracked() {
|
|
99
|
-
try {
|
|
100
|
-
// git ls-files --error-unmatch .env
|
|
101
|
-
(0, child_process_1.execSync)("git ls-files --error-unmatch .env", { stdio: "ignore" });
|
|
102
|
-
return true; // Command succeeded, file is tracked
|
|
103
|
-
}
|
|
104
|
-
catch (e) {
|
|
105
|
-
return false; // Command failed, file not tracked
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
function getStagedFiles() {
|
|
109
|
-
try {
|
|
110
|
-
const output = (0, child_process_1.execSync)("git diff --cached --name-only", { encoding: "utf-8" });
|
|
111
|
-
return output.split("\n").filter(f => f.trim().length > 0);
|
|
112
|
-
}
|
|
113
|
-
catch (e) {
|
|
114
|
-
return [];
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
function getAllFiles() {
|
|
118
|
-
// Naive implementation for now, or recurse.
|
|
119
|
-
// Usually 'git ls-files' is better if generic scan.
|
|
120
|
-
try {
|
|
121
|
-
const output = (0, child_process_1.execSync)("git ls-files", { encoding: "utf-8" });
|
|
122
|
-
return output.split("\n").filter(f => f.trim().length > 0);
|
|
123
|
-
}
|
|
124
|
-
catch (e) {
|
|
125
|
-
return [];
|
|
126
|
-
}
|
|
127
|
-
}
|
|
1
|
+
var y=Object.create;var c=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var k=Object.getOwnPropertyNames;var x=Object.getPrototypeOf,b=Object.prototype.hasOwnProperty;var v=(e,t)=>{for(var n in t)c(e,n,{get:t[n],enumerable:!0})},h=(e,t,n,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of k(t))!b.call(e,s)&&s!==n&&c(e,s,{get:()=>t[s],enumerable:!(r=S(t,s))||r.enumerable});return e};var d=(e,t,n)=>(n=e!=null?y(x(e)):{},h(t||!e||!e.__esModule?c(n,"default",{value:e,enumerable:!0}):n,e)),C=e=>h(c({},"__esModule",{value:!0}),e);var A={};v(A,{scanCommand:()=>F});module.exports=C(A);var m=require("commander"),o=d(require("chalk")),i=d(require("fs")),f=d(require("path")),a=require("child_process"),F=new m.Command("scan").description("Scan project for secrets and configuration leaks").option("--staged","Scan only staged files (for pre-commit hooks)",!1).option("--install-hook","Install the git pre-commit hook").action(async e=>{let{staged:t,installHook:n}=e;if(n){I();return}console.log(o.default.bold("XtraSync Secret Scan"));let r=0;j()&&(console.error(o.default.red("CRITICAL: .env file is tracked by git!")),r++);let s=t?E():X(),p=[{name:"Xtra API Key",regex:/xs_[a-zA-Z0-9]{10,}/}];s.forEach(l=>{try{if(!i.default.existsSync(l))return;let u=i.default.readFileSync(l,"utf-8");p.forEach(g=>{g.regex.test(u)&&(console.error(o.default.red(`[${l}] Potential Secret Detected: ${g.name}`)),r++)})}catch{}}),r>0?(console.error(o.default.red(`
|
|
2
|
+
\u2716 Scan failed. found ${r} issues.`)),process.exit(1)):console.log(o.default.green(`
|
|
3
|
+
\u2714 Scan passed. No secrets found.`))});function I(){let e=f.default.join(process.cwd(),".git","hooks"),t=f.default.join(e,"pre-commit");if(!i.default.existsSync(e)){console.error(o.default.red("Error: .git directory not found. Is this a git repository?"));return}i.default.writeFileSync(t,`#!/bin/sh
|
|
4
|
+
# XtraSync Secret Scan
|
|
5
|
+
echo "Running XtraSync Secret Scan..."
|
|
6
|
+
|
|
7
|
+
# Check if 'xtra' is globally installed or use local script
|
|
8
|
+
if command -v xtra >/dev/null 2>&1; then
|
|
9
|
+
xtra scan --staged
|
|
10
|
+
elif [ -f "./node_modules/.bin/xtra" ]; then
|
|
11
|
+
./node_modules/.bin/xtra scan --staged
|
|
12
|
+
else
|
|
13
|
+
# Using the binary directly if running in dev environment (unlikely in prod use case)
|
|
14
|
+
# For dev testing:
|
|
15
|
+
if [ -f "./dist/bin/xtra.js" ]; then
|
|
16
|
+
node ./dist/bin/xtra.js scan --staged
|
|
17
|
+
else
|
|
18
|
+
echo "Warning: XtraSync CLI not found. Skipping scan."
|
|
19
|
+
fi
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
# Exit code of scan is passed through
|
|
23
|
+
`,{mode:493}),console.log(o.default.green("\u2714 Pre-commit hook installed successfully."))}function j(){try{return(0,a.execSync)("git ls-files --error-unmatch .env",{stdio:"ignore"}),!0}catch{return!1}}function E(){try{return(0,a.execSync)("git diff --cached --name-only",{encoding:"utf-8"}).split(`
|
|
24
|
+
`).filter(t=>t.trim().length>0)}catch{return[]}}function X(){try{return(0,a.execSync)("git ls-files",{encoding:"utf-8"}).split(`
|
|
25
|
+
`).filter(t=>t.trim().length>0)}catch{return[]}}0&&(module.exports={scanCommand});
|