securewithtrace 0.10.0 → 0.11.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.
Files changed (3) hide show
  1. package/README.md +25 -2
  2. package/dist/index.js +281 -29
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  `tracecli` is the official CLI for Trace.
12
12
 
13
- Use it to authenticate, browse connected repositories, clone repositories, and inspect vulnerabilities directly from the command line.
13
+ Use it to authenticate, browse and filter connected repositories, clone repositories, and inspect vulnerabilities directly from the command line.
14
14
 
15
15
  ## Install
16
16
 
@@ -42,6 +42,9 @@ tracecli auth status
42
42
  # 3) List connected repositories
43
43
  tracecli repo list
44
44
 
45
+ # 3b) Filter repositories
46
+ tracecli repo list --platform GITHUB --status CONNECTED --query trace
47
+
45
48
  # 4) View vulnerabilities
46
49
  tracecli vulnerabilities list --severity HIGH
47
50
  ```
@@ -49,7 +52,7 @@ tracecli vulnerabilities list --severity HIGH
49
52
  ## What You Can Do
50
53
 
51
54
  - **Authenticate securely** with device authorization flow (`auth login/logout/status`)
52
- - **List repositories** connected to your Trace organization (`repo list`)
55
+ - **List and filter repositories** connected to your Trace organization (`repo list`, `repo filter`)
53
56
  - **Clone repositories** with secure token handling (`repo clone`)
54
57
  - **Filter vulnerabilities** by repo, severity, and status (`vulnerabilities list`)
55
58
  - **Export results as JSON or TSV** for automation (`vulnerabilities list --output json`)
@@ -70,6 +73,15 @@ tracecli auth logout
70
73
  # List org repositories known to Trace
71
74
  tracecli repo list
72
75
 
76
+ # List only connected GitHub repositories that match "trace"
77
+ tracecli repo list --platform GITHUB --status CONNECTED --query trace
78
+
79
+ # Advanced filtering syntax (repeatable)
80
+ tracecli repo list --filter "platform=GITHUB" --filter "repo:trace" --filter "status!=DISCONNECTED"
81
+
82
+ # Alias command for filtering
83
+ tracecli repo filter --filter "language:typescript" --filter "branch=main"
84
+
73
85
  # Clone by owner/repo shorthand
74
86
  tracecli repo clone securewithtrace/trace
75
87
 
@@ -114,6 +126,17 @@ Supported filter values:
114
126
  - `--severity`: `CRITICAL`, `HIGH`, `MEDIUM`, `LOW`
115
127
  - `--status`: `OPEN`, `IN_PROGRESS`, `FIXED`, `IGNORED`, `FALSE_POSITIVE`
116
128
 
129
+ Repository filter options:
130
+
131
+ - `--platform`: `GITHUB`, `GITLAB`, `BITBUCKET`
132
+ - `--status`: `CONNECTED`, `DISCONNECTED`
133
+ - `--query`: case-insensitive contains match across URL, repo name, default branch, and language
134
+ - `--default-branch`: case-insensitive contains match for default branch
135
+ - `--language`: case-insensitive contains match for primary language
136
+ - `--filter`: repeatable advanced expression in the form `<field><operator><value>`
137
+ - Supported fields: `platform`, `status`, `url`, `repo`, `branch`, `language`
138
+ - Supported operators: `=` (exact), `!=` (not exact), `:` (contains), `!:` (not contains)
139
+
117
140
  ### Pentests
118
141
 
119
142
  ```bash
package/dist/index.js CHANGED
@@ -25736,6 +25736,9 @@ function extractRepoName(input) {
25736
25736
  return input;
25737
25737
  }
25738
25738
  var BRANCH_NAME_PATTERN = /^[a-zA-Z0-9._/+-]+$/;
25739
+ var TRACE_GIT_ASKPASS_TOKEN_ENV = "TRACE_GIT_ASKPASS_TOKEN";
25740
+ var REPOSITORY_PLATFORMS = ["GITHUB", "GITLAB", "BITBUCKET"];
25741
+ var REPOSITORY_STATUSES = ["CONNECTED", "DISCONNECTED"];
25739
25742
  function validateBranchName(branch) {
25740
25743
  if (branch.startsWith("-")) {
25741
25744
  throw new Error(`Invalid branch name: '${branch}' \u2014 branch names cannot start with a dash`);
@@ -25745,29 +25748,268 @@ function validateBranchName(branch) {
25745
25748
  }
25746
25749
  return branch;
25747
25750
  }
25748
- function createListCommand2() {
25749
- return new Command("list").description("List repositories in your organization").action(async () => {
25750
- const organizationId = await resolveOrganizationId();
25751
- const result = await authenticatedRequest(
25752
- (client) => client.repository.listRepositories({
25753
- params: { organizationId }
25754
- })
25751
+ function parseCloneDepth(value) {
25752
+ const depth = Number.parseInt(value, 10);
25753
+ if (!Number.isInteger(depth) || depth < 1) {
25754
+ throw new Error("--depth must be a positive integer");
25755
+ }
25756
+ return depth;
25757
+ }
25758
+ function normalizeForMatching(value) {
25759
+ return value.trim().toLowerCase();
25760
+ }
25761
+ function normalizePlatform(value, source) {
25762
+ const normalized = value.trim().toUpperCase();
25763
+ if (REPOSITORY_PLATFORMS.includes(normalized)) {
25764
+ return normalized;
25765
+ }
25766
+ throw new Error(
25767
+ `${source} must be one of: ${REPOSITORY_PLATFORMS.join(", ")}. Received '${value}'.`
25768
+ );
25769
+ }
25770
+ function normalizeStatus(value, source) {
25771
+ const normalized = value.trim().toUpperCase();
25772
+ if (REPOSITORY_STATUSES.includes(normalized)) {
25773
+ return normalized;
25774
+ }
25775
+ throw new Error(
25776
+ `${source} must be one of: ${REPOSITORY_STATUSES.join(", ")}. Received '${value}'.`
25777
+ );
25778
+ }
25779
+ function normalizeFilterField(rawField) {
25780
+ const field = rawField.trim().toLowerCase();
25781
+ switch (field) {
25782
+ case "platform":
25783
+ return "platform";
25784
+ case "status":
25785
+ return "status";
25786
+ case "url":
25787
+ case "repository":
25788
+ case "repositoryurl":
25789
+ case "repository_url":
25790
+ return "repository_url";
25791
+ case "repo":
25792
+ case "name":
25793
+ case "repository_name":
25794
+ return "repo_name";
25795
+ case "branch":
25796
+ case "defaultbranch":
25797
+ case "default_branch":
25798
+ return "default_branch";
25799
+ case "language":
25800
+ case "primarylanguage":
25801
+ case "primary_language":
25802
+ return "language";
25803
+ default:
25804
+ throw new Error(
25805
+ `Unknown repository filter field '${rawField}'. Supported fields: platform, status, url, repo, branch, language.`
25806
+ );
25807
+ }
25808
+ }
25809
+ function parseFilterOperator(rawOperator) {
25810
+ switch (rawOperator) {
25811
+ case "=":
25812
+ return "exact";
25813
+ case "!=":
25814
+ return "not_exact";
25815
+ case ":":
25816
+ return "contains";
25817
+ case "!:":
25818
+ return "not_contains";
25819
+ default:
25820
+ throw new Error(`Unsupported filter operator '${rawOperator}'. Use one of: =, !=, :, !:.`);
25821
+ }
25822
+ }
25823
+ function parseRepositoryFilterExpression(expression) {
25824
+ const trimmed = expression.trim();
25825
+ const match = /^([a-zA-Z][a-zA-Z0-9_-]*)(!=|!:|=|:)(.+)$/.exec(trimmed);
25826
+ if (!match) {
25827
+ throw new Error(
25828
+ `Invalid filter '${expression}'. Expected format: <field><operator><value> (e.g., platform=GITHUB, repo:trace, status!=DISCONNECTED).`
25755
25829
  );
25756
- if (result.status !== 200) {
25757
- console.error(`Failed to list repositories (status ${result.status}).`);
25758
- process.exit(1);
25830
+ }
25831
+ const rawField = match[1];
25832
+ const rawOperator = match[2];
25833
+ const rawValue = match[3];
25834
+ if (!rawField || !rawOperator || !rawValue) {
25835
+ throw new Error(`Invalid filter '${expression}'.`);
25836
+ }
25837
+ const value = rawValue.trim();
25838
+ if (!value) {
25839
+ throw new Error(`Invalid filter '${expression}': value cannot be empty.`);
25840
+ }
25841
+ const field = normalizeFilterField(rawField);
25842
+ const operator = parseFilterOperator(rawOperator);
25843
+ if (field === "platform") {
25844
+ normalizePlatform(value, "--filter");
25845
+ }
25846
+ if (field === "status") {
25847
+ normalizeStatus(value, "--filter");
25848
+ }
25849
+ return { field, operator, value };
25850
+ }
25851
+ function doesValueMatchFilter(candidate, filterValue, operator, exactMatchOnly) {
25852
+ const normalizedCandidate = normalizeForMatching(candidate);
25853
+ const normalizedFilter = normalizeForMatching(filterValue);
25854
+ if (exactMatchOnly || operator === "exact" || operator === "not_exact") {
25855
+ const matches2 = normalizedCandidate === normalizedFilter;
25856
+ return operator === "not_exact" ? !matches2 : matches2;
25857
+ }
25858
+ const matches = normalizedCandidate.includes(normalizedFilter);
25859
+ return operator === "not_contains" ? !matches : matches;
25860
+ }
25861
+ function matchParsedFilter(repo, parsedFilter) {
25862
+ switch (parsedFilter.field) {
25863
+ case "platform":
25864
+ return doesValueMatchFilter(repo.platform, parsedFilter.value, parsedFilter.operator, true);
25865
+ case "status":
25866
+ return doesValueMatchFilter(repo.status, parsedFilter.value, parsedFilter.operator, true);
25867
+ case "repository_url":
25868
+ return doesValueMatchFilter(
25869
+ repo.repositoryUrl,
25870
+ parsedFilter.value,
25871
+ parsedFilter.operator,
25872
+ false
25873
+ );
25874
+ case "repo_name":
25875
+ return doesValueMatchFilter(
25876
+ extractRepoName(repo.repositoryUrl),
25877
+ parsedFilter.value,
25878
+ parsedFilter.operator,
25879
+ false
25880
+ );
25881
+ case "default_branch":
25882
+ return doesValueMatchFilter(
25883
+ repo.defaultBranch,
25884
+ parsedFilter.value,
25885
+ parsedFilter.operator,
25886
+ false
25887
+ );
25888
+ case "language": {
25889
+ const language = repo.primaryLanguage ?? "";
25890
+ return doesValueMatchFilter(language, parsedFilter.value, parsedFilter.operator, false);
25759
25891
  }
25760
- const repos = result.body;
25761
- if (repos.length === 0) {
25762
- console.log("No repositories found.");
25763
- return;
25892
+ }
25893
+ }
25894
+ function filterRepositories(repositories, options) {
25895
+ const parsedFilters = (options.filters ?? []).map(
25896
+ (expression) => parseRepositoryFilterExpression(expression)
25897
+ );
25898
+ const normalizedPlatform = options.platform ? normalizePlatform(options.platform, "--platform") : void 0;
25899
+ const normalizedStatus = options.status ? normalizeStatus(options.status, "--status") : void 0;
25900
+ const normalizedQuery = options.query ? normalizeForMatching(options.query) : void 0;
25901
+ const normalizedDefaultBranch = options.defaultBranch ? normalizeForMatching(options.defaultBranch) : void 0;
25902
+ const normalizedLanguage = options.language ? normalizeForMatching(options.language) : void 0;
25903
+ return repositories.filter((repo) => {
25904
+ if (normalizedPlatform && repo.platform.toUpperCase() !== normalizedPlatform) {
25905
+ return false;
25906
+ }
25907
+ if (normalizedStatus && repo.status.toUpperCase() !== normalizedStatus) {
25908
+ return false;
25909
+ }
25910
+ if (normalizedDefaultBranch && !normalizeForMatching(repo.defaultBranch).includes(normalizedDefaultBranch)) {
25911
+ return false;
25764
25912
  }
25765
- for (const repo of repos) {
25766
- const status = repo.status === "CONNECTED" ? "connected" : "disconnected";
25767
- const url2 = stripAnsi(String(repo.repositoryUrl));
25768
- const platform = stripAnsi(String(repo.platform)).toLowerCase();
25769
- console.log(`${url2} (${platform}, ${status})`);
25913
+ const repoLanguage = normalizeForMatching(repo.primaryLanguage ?? "");
25914
+ if (normalizedLanguage && !repoLanguage.includes(normalizedLanguage)) {
25915
+ return false;
25770
25916
  }
25917
+ if (normalizedQuery) {
25918
+ const normalizedUrl = normalizeForMatching(repo.repositoryUrl);
25919
+ const normalizedBranch = normalizeForMatching(repo.defaultBranch);
25920
+ const normalizedName = normalizeForMatching(extractRepoName(repo.repositoryUrl));
25921
+ if (!normalizedUrl.includes(normalizedQuery) && !normalizedBranch.includes(normalizedQuery) && !repoLanguage.includes(normalizedQuery) && !normalizedName.includes(normalizedQuery)) {
25922
+ return false;
25923
+ }
25924
+ }
25925
+ for (const parsedFilter of parsedFilters) {
25926
+ if (!matchParsedFilter(repo, parsedFilter)) {
25927
+ return false;
25928
+ }
25929
+ }
25930
+ return true;
25931
+ });
25932
+ }
25933
+ function collectFilterExpression(value, previous) {
25934
+ previous.push(value);
25935
+ return previous;
25936
+ }
25937
+ function toRepositoryListFilterOptions(options) {
25938
+ return {
25939
+ ...options.platform ? { platform: options.platform } : {},
25940
+ ...options.status ? { status: options.status } : {},
25941
+ ...options.query ? { query: options.query } : {},
25942
+ ...options.defaultBranch ? { defaultBranch: options.defaultBranch } : {},
25943
+ ...options.language ? { language: options.language } : {},
25944
+ ...options.filter.length > 0 ? { filters: options.filter } : {}
25945
+ };
25946
+ }
25947
+ function addRepositoryFilterOptions(command) {
25948
+ return command.option(
25949
+ "--platform <platform>",
25950
+ "Filter by platform (GITHUB, GITLAB, BITBUCKET)",
25951
+ (value) => value.toUpperCase()
25952
+ ).option(
25953
+ "--status <status>",
25954
+ "Filter by status (CONNECTED, DISCONNECTED)",
25955
+ (value) => value.toUpperCase()
25956
+ ).option("--query <query>", "Case-insensitive search across URL, repo, branch, and language").option("--default-branch <branch>", "Filter by default branch (case-insensitive contains)").option("--language <language>", "Filter by primary language (case-insensitive contains)").option(
25957
+ "--filter <expression>",
25958
+ "Advanced filter expression. Repeatable. Examples: platform=GITHUB, repo:trace, status!=DISCONNECTED, language!:typescript",
25959
+ collectFilterExpression,
25960
+ []
25961
+ ).option("--json", "Output as JSON");
25962
+ }
25963
+ async function runRepositoryListCommand(options) {
25964
+ let filteredRepositories;
25965
+ const organizationId = await resolveOrganizationId();
25966
+ const result = await authenticatedRequest(
25967
+ (client) => client.repository.listRepositories({
25968
+ params: { organizationId }
25969
+ })
25970
+ );
25971
+ if (result.status !== 200) {
25972
+ console.error(`Failed to list repositories (status ${result.status}).`);
25973
+ process.exit(1);
25974
+ }
25975
+ try {
25976
+ filteredRepositories = filterRepositories(result.body, toRepositoryListFilterOptions(options));
25977
+ } catch (error48) {
25978
+ const message = error48 instanceof Error ? error48.message : "Unknown filter parsing error.";
25979
+ console.error(`Invalid repository filter: ${message}`);
25980
+ process.exit(1);
25981
+ }
25982
+ if (filteredRepositories.length === 0) {
25983
+ if (options.platform || options.status || options.query || options.defaultBranch || options.language || options.filter.length > 0) {
25984
+ console.log("No repositories found matching the provided filters.");
25985
+ return;
25986
+ }
25987
+ console.log("No repositories found.");
25988
+ return;
25989
+ }
25990
+ if (options.json) {
25991
+ console.log(JSON.stringify(filteredRepositories, null, 2));
25992
+ return;
25993
+ }
25994
+ for (const repo of filteredRepositories) {
25995
+ const status = repo.status === "CONNECTED" ? "connected" : "disconnected";
25996
+ const url2 = stripAnsi(String(repo.repositoryUrl));
25997
+ const platform = stripAnsi(String(repo.platform)).toLowerCase();
25998
+ console.log(`${url2} (${platform}, ${status})`);
25999
+ }
26000
+ }
26001
+ function createListCommand2() {
26002
+ return addRepositoryFilterOptions(
26003
+ new Command("list").description("List repositories in your organization")
26004
+ ).action(async (options) => {
26005
+ await runRepositoryListCommand(options);
26006
+ });
26007
+ }
26008
+ function createFilterCommand() {
26009
+ return addRepositoryFilterOptions(
26010
+ new Command("filter").description("Filter repositories in your organization")
26011
+ ).action(async (options) => {
26012
+ await runRepositoryListCommand(options);
25771
26013
  });
25772
26014
  }
25773
26015
  function urlWithUsername(httpsUrl) {
@@ -25776,14 +26018,21 @@ function urlWithUsername(httpsUrl) {
25776
26018
  url2.password = "";
25777
26019
  return url2.toString();
25778
26020
  }
25779
- function createAskPassScript(token) {
26021
+ function createAskPassScript() {
25780
26022
  const dir = mkdtempSync(join3(tmpdir(), "trace-askpass-"));
25781
26023
  const scriptPath = join3(dir, "askpass.sh");
25782
- writeFileSync3(scriptPath, `#!/bin/sh
25783
- echo '${token.replace(/'/g, "'\\''")}'
25784
- `, {
25785
- mode: 448
25786
- });
26024
+ writeFileSync3(
26025
+ scriptPath,
26026
+ `#!/bin/sh
26027
+ if [ -z "\${${TRACE_GIT_ASKPASS_TOKEN_ENV}:-}" ]; then
26028
+ exit 1
26029
+ fi
26030
+ printf '%s\\n' "$${TRACE_GIT_ASKPASS_TOKEN_ENV}"
26031
+ `,
26032
+ {
26033
+ mode: 448
26034
+ }
26035
+ );
25787
26036
  chmodSync(scriptPath, 448);
25788
26037
  return scriptPath;
25789
26038
  }
@@ -25803,7 +26052,8 @@ async function fetchCloneToken(repoUrl) {
25803
26052
  return null;
25804
26053
  }
25805
26054
  if (result.status === 200) {
25806
- return result.body.token;
26055
+ const token = result.body.token.trim();
26056
+ return token.length > 0 ? token : null;
25807
26057
  }
25808
26058
  return null;
25809
26059
  } catch {
@@ -25811,7 +26061,7 @@ async function fetchCloneToken(repoUrl) {
25811
26061
  }
25812
26062
  }
25813
26063
  function createCloneCommand() {
25814
- return new Command("clone").description("Clone a repository").argument("<repository>", "Repository URL or owner/repo shorthand").argument("[directory]", "Directory to clone into").option("--branch <branch>", "Branch to clone").option("--depth <depth>", "Shallow clone depth", Number.parseInt).action(
26064
+ return new Command("clone").description("Clone a repository").argument("<repository>", "Repository URL or owner/repo shorthand").argument("[directory]", "Directory to clone into").option("--branch <branch>", "Branch to clone").option("--depth <depth>", "Shallow clone depth", parseCloneDepth).action(
25815
26065
  async (repository, directory, options) => {
25816
26066
  const cloneUrl = resolveRepoUrl(repository);
25817
26067
  const targetDir = directory ?? extractRepoName(repository);
@@ -25833,8 +26083,9 @@ function createCloneCommand() {
25833
26083
  console.log(`Cloning ${cloneUrl} into ${targetDir}...`);
25834
26084
  const extraEnv = {};
25835
26085
  if (installationToken) {
25836
- askPassScript = createAskPassScript(installationToken);
26086
+ askPassScript = createAskPassScript();
25837
26087
  extraEnv["GIT_ASKPASS"] = askPassScript;
26088
+ extraEnv[TRACE_GIT_ASKPASS_TOKEN_ENV] = installationToken;
25838
26089
  extraEnv["GIT_TERMINAL_PROMPT"] = "0";
25839
26090
  }
25840
26091
  try {
@@ -25845,7 +26096,7 @@ function createCloneCommand() {
25845
26096
  });
25846
26097
  const exitCode = await proc.exited;
25847
26098
  if (exitCode !== 0) {
25848
- process.exit(exitCode);
26099
+ process.exit(1);
25849
26100
  }
25850
26101
  if (installationToken) {
25851
26102
  const resetProc = Bun.spawn(["git", "remote", "set-url", "origin", cloneUrl], {
@@ -25872,6 +26123,7 @@ function createCloneCommand() {
25872
26123
  function createRepoCommand() {
25873
26124
  const repo = new Command("repo").description("Manage repositories");
25874
26125
  repo.addCommand(createListCommand2());
26126
+ repo.addCommand(createFilterCommand());
25875
26127
  repo.addCommand(createCloneCommand());
25876
26128
  return repo;
25877
26129
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "securewithtrace",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Official Trace CLI for vulnerability intelligence in your terminal",
5
5
  "type": "module",
6
6
  "bin": {