diff --git a/.env_template b/.env_template new file mode 100644 index 0000000..5761b3d --- /dev/null +++ b/.env_template @@ -0,0 +1,7 @@ +FLASK_APP=issues.py +FLASK_DEBUG=0 + +GIT_REPO_URL=https://github.com/api/v3/repos/USERNAME/REPO/issues +GIT_ACCESS_TOKEN=PERSONAL_ACCESS_TOKEN + +LGTM_SECRET=SECRET_AS_SPECIFIED_IN_LGTM_INTEGRATION_PANEL diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..11614af --- /dev/null +++ b/.gitignore @@ -0,0 +1,115 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..ed69d4a --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,39 @@ +# Code of Conduct + +This code of conduct outlines expectations for participation in the Semmle open source community, including any open source repositories on GitHub.com, as well as steps for reporting unacceptable behavior. We are committed to providing a welcoming and inspiring community for all. + +People violating this code of conduct may be banned from the community. + +Our community strives to: +* Be friendly and patient: Remember you might not be communicating in someone else’s primary spoken or programming language, and others may not have your level of understanding. +* Be welcoming: Our community welcomes and supports people of all backgrounds and identities. This includes, but is not limited to members of any race, ethnicity, culture, national origin, color, immigration status, social and economic class, educational level, sex, sexual orientation, gender identity and expression, age, size, family status, political belief, religion, and mental and physical ability. +* Be respectful: We are a world-wide community of professionals, and we conduct ourselves professionally. Disagreement is no excuse for poor behavior and poor manners. Disrespectful and unacceptable behavior includes, but is not limited to: + * Violent threats or language. + * Discriminatory or derogatory jokes and language. + * Posting sexually explicit or violent material. + * Posting, or threatening to post, people’s personally identifying information (“doxing”). + * Insults, especially those using discriminatory terms or slurs. + * Behavior that could be perceived as sexual attention. +* Advocating for or encouraging any of the above behaviors. +* Understand disagreements: Disagreements, both social and technical, are useful learning opportunities. Seek to understand others’ viewpoints and resolve differences constructively. + +This code is not exhaustive or complete. It serves to capture our common understanding of a productive, collaborative environment. We expect the code to be followed in spirit as much as in the letter. + +# Scope + +This code of conduct applies to all repositories and communities for Semmle open source projects, regardless of whether or not the repository explicitly calls out its use of this code. The code also applies in public spaces when an individual is representing the Semmle open source community. Examples include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. + + +# Reporting Code of Conduct Issues +We encourage members of the community to resolve issues on their own whenever possible. This builds a broader and deeper understanding and ultimately a healthier interaction. In the event that an issue cannot be resolved locally, please feel free to report your concerns by contacting code-of-conduct@semmle.com. +In your report please include: +* Your contact information. +* Names (real, usernames or pseudonyms) of any individuals involved. If there are additional witnesses, please include them as well. +* Your account of what occurred, and if you believe the incident is ongoing. If there is a publicly available record (e.g. a mailing list archive or a public chat log), please include a link or attachment. +* Any additional information that may be helpful. + +All reports will be reviewed by a multi-person team and will result in a response that is deemed necessary and appropriate to the circumstances. Where additional perspectives are needed, the team may seek insight from others with relevant expertise or experience. The confidentiality of the person reporting the incident will be kept at all times. Involved parties are never part of the review team. + +Anyone asked to stop unacceptable behavior is expected to comply immediately. If an individual engages in unacceptable behavior, the review team may take any action they deem appropriate, including a permanent ban from the community. + +*This text is licensed under the [CC-BY-4.0](https://creativecommons.org/licenses/by/4.0/) license. It is based on a template established by the [TODO Group](http://todogroup.org/) and variants thereof used by numerous other large communities (e.g., [Microsoft](https://microsoft.github.io/codeofconduct/), [Facebook](https://code.fb.com/codeofconduct/), [Yahoo](https://yahoo.github.io/codeofconduct), [Twitter](https://github.com/twitter/code-of-conduct), [GitHub](https://blog.github.com/2015-07-20-adopting-the-open-code-of-conduct/)) and the Scope section from the [Contributor Covenant version 1.4](http://contributor-covenant.org/version/1/4/).* diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..5c9fdd3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,46 @@ +# Contributing to LGTM Issue Tracker Example + +We welcome contributions to our example LGTM Issue Tracker. While we intend this project to remain a minimal pedagogical example, if you have an idea how it could be made clearer or more valuable to other users, then please go ahead an open a Pull Request! + +Before we accept your pull request, we will require that you have agreed to our Contributor License Agreement, this is not something that you need to do before you submit your pull request, but until you've done so, we will be unable to accept your contribution. + +## Using your personal data + +If you contribute to this project, we will record your name and email +address (as provided by you with your contributions) as part of the code +repositories, which might be made public. We might also use this information +to contact you in relation to your contributions, as well as in the +normal course of software development. We also store records of your +CLA agreements. Under GDPR legislation, we do this +on the basis of our legitimate interest in creating the QL product. + +Please do get in touch (privacy@semmle.com) if you have any questions about +this or our data protection policies. + +## Contributor License Agreement + +This Contributor License Agreement (“Agreement”) is entered into between Semmle Limited (“Semmle,” “we” or “us” etc.), and You (as defined and further identified below). + +Accordingly, You hereby agree to the following terms for Your present and future Contributions submitted to Semmle: + +1. **Definitions**. + + * "You" (or "Your") shall mean the Contribution copyright owner (whether an individual or organization) or legal entity authorized by the copyright owner that is making this Agreement with Semmle. For legal entities, the entity making a Contribution and all other entities that control, are controlled by, or are under common control with that entity are considered to be a single Contributor. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + + * "Contribution(s)" shall mean the code, documentation or other original works of authorship, including any modifications or additions to an existing work, submitted by You to Semmle for inclusion in, or documentation of, any of the products or projects owned or managed by Semmle (the "Work(s)"). For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to Semmle or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, Semmle for the purpose of discussing and/or improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by You as "Not a Contribution." + +2. **Grant of Copyright License**. You hereby grant to Semmle and to recipients of software distributed by Semmle a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute Your Contributions and such derivative works. + +3. **Grant of Patent License**. You hereby grant to Semmle and to recipients of software distributed by Semmle a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by You that are necessarily infringed by Your Contribution(s) alone or by combination of Your Contribution(s) with the Work to which such Contribution(s) was submitted. If any entity institutes patent litigation against You or any other entity (including a cross-claim or counterclaim in a lawsuit) alleging that Your Contribution, or the Work to which You have contributed, constitutes direct or contributory patent infringement, then any patent licenses granted to that entity under this Agreement for that Contribution or Work shall terminate as of the date such litigation is filed. + +4. **Ownership**. Except as set out above, You keep all right, title, and interest in Your Contribution. The rights that You grant to us under this Agreement are effective on the date You first submitted a Contribution to us, even if Your submission took place before the date You entered this Agreement. + +5. **Representations**. You represent and warrant that: (i) the Contributions are an original work and that You can legally grant the rights set out in this Agreement; (ii) the Contributions and Semmle’s exercise of any license rights granted hereunder, does not and will not, infringe the rights of any third party; (iii) You are not aware of any pending or threatened claims, suits, actions, or charges pertaining to the Contributions, including without limitation any claims or allegations that any or all of the Contributions infringes, violates, or misappropriate the intellectual property rights of any third party (You further agree that You will notify Semmle immediately if You become aware of any such actual or potential claims, suits, actions, allegations or charges). + +6. **Employer**. If Your employer(s) has rights to intellectual property that You create that includes Your Contributions, You represent and warrant that Your employer has waived such rights for Your Contributions to Semmle, or that You have received permission to make Contributions on behalf of that employer and that You are authorized to execute this Agreement on behalf of Your employer. + +7. **Inclusion of Code**. We determine the code that is in our Works. You understand that the decision to include the Contribution in any project or source repository is entirely that of Semmle, and this agreement does not guarantee that the Contributions will be included in any product. + +8. **Disclaimer**. You are not expected to provide support for Your Contributions, except to the extent You desire to provide support. You may provide support for free, for a fee, or not at all. Except as set forth herein, and unless required by applicable law or agreed to in writing, You provide Your Contributions on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND. + +9. **General**. The failure of either party to enforce its rights under this Agreement for any period shall not be construed as a waiver of such rights. No changes or modifications or waivers to this Agreement will be effective unless in writing and signed by both parties. In the event that any provision of this Agreement shall be determined to be illegal or unenforceable, that provision will be limited or eliminated to the minimum extent necessary so that this Agreement shall otherwise remain in full force and effect and enforceable. This Agreement shall be governed by and construed in accordance with the laws of the State of California in the United States without regard to the conflicts of laws provisions thereof. In any action or proceeding to enforce rights under this Agreement, the prevailing party will be entitled to recover costs and attorneys’ fees. \ No newline at end of file diff --git a/COPYRIGHT b/COPYRIGHT new file mode 100644 index 0000000..65d947f --- /dev/null +++ b/COPYRIGHT @@ -0,0 +1,13 @@ +Copyright (c) Semmle Inc and other contributors. All rights reserved. + +Licensed under the Apache License, Version 2.0 (the "License"); you may not use +this file except in compliance with the License. You may obtain a copy of the +License at http://www.apache.org/licenses/LICENSE-2.0 + +THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED +WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, +MERCHANTABLITY OR NON-INFRINGEMENT. + +See the Apache Version 2.0 License for specific language governing permissions +and limitations under the License. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d9a10c0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,176 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/Pipfile b/Pipfile new file mode 100644 index 0000000..4e6db0e --- /dev/null +++ b/Pipfile @@ -0,0 +1,11 @@ +[[source]] +name = "pypi" +url = "https://pypi.org/simple" +verify_ssl = true + +[packages] +flask = "*" +requests = "*" + +[requires] +python_version = "3.5" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 0000000..a06424d --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,126 @@ +{ + "_meta": { + "hash": { + "sha256": "97ada2aab28892e5fcee35a2662aa8060b3640bc883ced558533af72e865ff1d" + }, + "pipfile-spec": 6, + "requires": { + "python_version": "3.5" + }, + "sources": [ + { + "name": "pypi", + "url": "https://pypi.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "click": { + "hashes": [ + "sha256:2335065e6395b9e67ca716de5f7526736bfa6ceead690adf616d925bdc622b13", + "sha256:5b94b49521f6456670fdb30cd82a4eca9412788a93fa6dd6df72c94d5a8ff2d7" + ], + "version": "==7.0" + }, + "flask": { + "hashes": [ + "sha256:2271c0070dbcb5275fad4a82e29f23ab92682dc45f9dfbc22c02ba9b9322ce48", + "sha256:a080b744b7e345ccfcbc77954861cb05b3c63786e93f2b3875e0913d44b43f05" + ], + "index": "pypi", + "version": "==1.0.2" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "itsdangerous": { + "hashes": [ + "sha256:321b033d07f2a4136d3ec762eac9f16a10ccd60f53c0c91af90217ace7ba1f19", + "sha256:b12271b2047cb23eeb98c8b5622e2e5c5e9abd9784a153e9d8ef9cb4dd09d749" + ], + "version": "==1.1.0" + }, + "jinja2": { + "hashes": [ + "sha256:74c935a1b8bb9a3947c50a54766a969d4846290e1e788ea44c1392163723c3bd", + "sha256:f84be1bb0040caca4cea721fcbbbbd61f9be9464ca236387158b0feea01914a4" + ], + "version": "==2.10" + }, + "markupsafe": { + "hashes": [ + "sha256:048ef924c1623740e70204aa7143ec592504045ae4429b59c30054cb31e3c432", + "sha256:130f844e7f5bdd8e9f3f42e7102ef1d49b2e6fdf0d7526df3f87281a532d8c8b", + "sha256:19f637c2ac5ae9da8bfd98cef74d64b7e1bb8a63038a3505cd182c3fac5eb4d9", + "sha256:1b8a7a87ad1b92bd887568ce54b23565f3fd7018c4180136e1cf412b405a47af", + "sha256:1c25694ca680b6919de53a4bb3bdd0602beafc63ff001fea2f2fc16ec3a11834", + "sha256:1f19ef5d3908110e1e891deefb5586aae1b49a7440db952454b4e281b41620cd", + "sha256:1fa6058938190ebe8290e5cae6c351e14e7bb44505c4a7624555ce57fbbeba0d", + "sha256:31cbb1359e8c25f9f48e156e59e2eaad51cd5242c05ed18a8de6dbe85184e4b7", + "sha256:3e835d8841ae7863f64e40e19477f7eb398674da6a47f09871673742531e6f4b", + "sha256:4e97332c9ce444b0c2c38dd22ddc61c743eb208d916e4265a2a3b575bdccb1d3", + "sha256:525396ee324ee2da82919f2ee9c9e73b012f23e7640131dd1b53a90206a0f09c", + "sha256:52b07fbc32032c21ad4ab060fec137b76eb804c4b9a1c7c7dc562549306afad2", + "sha256:52ccb45e77a1085ec5461cde794e1aa037df79f473cbc69b974e73940655c8d7", + "sha256:5c3fbebd7de20ce93103cb3183b47671f2885307df4a17a0ad56a1dd51273d36", + "sha256:5e5851969aea17660e55f6a3be00037a25b96a9b44d2083651812c99d53b14d1", + "sha256:5edfa27b2d3eefa2210fb2f5d539fbed81722b49f083b2c6566455eb7422fd7e", + "sha256:7d263e5770efddf465a9e31b78362d84d015cc894ca2c131901a4445eaa61ee1", + "sha256:83381342bfc22b3c8c06f2dd93a505413888694302de25add756254beee8449c", + "sha256:857eebb2c1dc60e4219ec8e98dfa19553dae33608237e107db9c6078b1167856", + "sha256:98e439297f78fca3a6169fd330fbe88d78b3bb72f967ad9961bcac0d7fdd1550", + "sha256:bf54103892a83c64db58125b3f2a43df6d2cb2d28889f14c78519394feb41492", + "sha256:d9ac82be533394d341b41d78aca7ed0e0f4ba5a2231602e2f05aa87f25c51672", + "sha256:e982fe07ede9fada6ff6705af70514a52beb1b2c3d25d4e873e82114cf3c5401", + "sha256:edce2ea7f3dfc981c4ddc97add8a61381d9642dc3273737e756517cc03e84dd6", + "sha256:efdc45ef1afc238db84cb4963aa689c0408912a0239b0721cb172b4016eb31d6", + "sha256:f137c02498f8b935892d5c0172560d7ab54bc45039de8805075e19079c639a9c", + "sha256:f82e347a72f955b7017a39708a3667f106e6ad4d10b25f237396a7115d8ed5fd", + "sha256:fb7c206e01ad85ce57feeaaa0bf784b97fa3cad0d4a5737bc5295785f5c613a1" + ], + "version": "==1.1.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "index": "pypi", + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + }, + "werkzeug": { + "hashes": [ + "sha256:c3fd7a7d41976d9f44db327260e263132466836cef6f91512889ed60ad26557c", + "sha256:d5da73735293558eb1651ee2fddc4d0dedcfa06538b8813a2e20011583c9e49b" + ], + "version": "==0.14.1" + } + }, + "develop": {} +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..be515bc --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# LGTM Issue Tracker Example +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/ambv/black) [![Total alerts](https://img.shields.io/lgtm/alerts/g/Semmle/lgtm-issue-tracker-example.svg?logo=lgtm&logoWidth=18)](https://lgtm.com/projects/g/Semmle/lgtm-issue-tracker-example/alerts/) + +This project gives a quick illustrative example showing how to integrate LGTM Enterprise with a 3rd-party issue tracker. This code is intended as a proof-of-concept only, showing the basic operations necessary to handle incoming requests from LGTM. It is not intended for production use. Please feel free to use this as a starting point for your own integration, but if you are using Atlassian Jira see also the [LGTM Jira Add-on](https://github.com/Semmle/lgtm-jira-addon). + +We use a lightweight `Flask` server to handle incoming requests, which in turn writes to the issue tracker of a specified Github repository. When not run in debug mode, incoming requests are verified using the secret specified when configuring the integration. For a more detailed explanation please see the associated [tutorial](tutorial.md). + +For instructions on configuring your LGTM Enterprise instance please see the relevant [LGTM help pages](https://help.semmle.com/lgtm-enterprise/admin/help/adding-issue-trackers.html). + +Requires an access token for the github installation with appropriate permissions. + +## Configuration + +When run through `pipenv` the app will pull config from the `.env` file, for which an example is provided... +```bash +FLASK_APP=issues.py +FLASK_DEBUG=0 + +GIT_REPO_URL=https://github.com/api/v3/repos/USERNAME/REPO/issues +GIT_ACCESS_TOKEN=PERSONAL_ACCESS_TOKEN + +LGTM_SECRET=SECRET_AS_SPECIFIED_IN_LGTM_INTEGRATION_PANEL +``` + +## Running +Easiest way to just get it running is with `pipenv`. Obviously a proper deployment would require something other than the built-in `Flask` development server, but for POC purposes... + +N.B. This example project requires a minimum of `python3.5`. + +```bash +pipenv install +pipenv run flask run +``` + +## Contributing + +We welcome contributions to our example LGTM Issue Tracker. While we intend this project to remain a minimal pedagogical example, if you have an idea how it could be made clearer or more valuable to other users, then please go ahead an open a Pull Request! Before you do, though, please take the time to read our [contributing guidelines](CONTRIBUTING.md). + +## License + +The LGTM Jira Add-on is licensed under [Apache License 2.0](LICENSE) by [Semmle](https://semmle.com). diff --git a/issues.py b/issues.py new file mode 100644 index 0000000..7e2d816 --- /dev/null +++ b/issues.py @@ -0,0 +1,88 @@ +import os +from flask import Flask, request, jsonify + +import requests +import json +import hmac + +URL = os.getenv("GIT_REPO_URL") +assert URL != None + +ACCESS_TOKEN = os.getenv("GIT_ACCESS_TOKEN") +assert ACCESS_TOKEN != None + +KEY = os.getenv("LGTM_SECRET", "").encode("utf-8") +assert KEY != "".encode("utf-8") + +headers = { + "content-type": "application/json", + "Authorization": "Bearer %s" % ACCESS_TOKEN, +} + +app = Flask(__name__) + + +def get_issue_dict(alert, project): + + title = "%s (%s)" % (alert["query"]["name"], project["name"]) + + lines = [] + lines.append("[%s](%s)" % (alert["query"]["name"], alert["query"]["url"])) + lines.append("") + lines.append("In %s:" % alert["file"]) + lines.append("> " + "\n> ".join(alert["message"].split("\n"))) + lines.append("[View alert on LGTM](%s)" % alert["url"]) + + return {"title": title, "body": "\n".join(lines), "labels": ["LGTM"]} + + +@app.route("/", methods=["POST"]) +def issues_webhook(): + + if not app.debug: + + digest = hmac.new(KEY, request.data, "sha1").hexdigest() + signature = request.headers.get("X-LGTM-Signature", "not-provided") + + if not hmac.compare_digest(signature, digest): + return jsonify({"message": "Unauthorized"}), 401 + + json_dict = request.get_json() + + transition = json_dict.get("transition") + + if transition == "create": + + data = get_issue_dict(json_dict.get("alert"), json_dict.get("project")) + + r = requests.post(URL, data=json.dumps(data), headers=headers) + + issue_id = r.json()["number"] + + else: + + if transition not in ["close", "reopen"]: + return ( + jsonify({"message": "unknown transition type - %s" % transition}), + 400, + ) + + issue_id = json_dict.get("issue-id") + + if issue_id is None: + return jsonify({"message": "no issue-id provided"}), 400 + + # handle a mistmatch between terminology on LGTM and Github + if transition == "reopen": + transition = "open" + + r = requests.patch( + os.path.sep.join([URL, str(issue_id)]), + data=json.dumps({"state": transition}), + headers=headers, + ) + + if not r.ok: + return r.content, r.status_code + + return jsonify({"issue-id": issue_id}), r.status_code diff --git a/lgtm.yml b/lgtm.yml new file mode 100644 index 0000000..b06edf3 --- /dev/null +++ b/lgtm.yml @@ -0,0 +1,4 @@ +extraction: + python: + python_setup: + version: 3 diff --git a/tutorial.md b/tutorial.md new file mode 100644 index 0000000..c7f134a --- /dev/null +++ b/tutorial.md @@ -0,0 +1,270 @@ + +# Integrating a 3rd-party issue tracker with LGTM + +To enable the creation of tickets in 3rd-party issue trackers, LGTM Enterprise offers a lightweight webhook integration. When enabled this will send outgoing POST requests to a specified endpoint, detailing new and changed alerts. + +We are going to outline a barebones implementation of a webapp that will process incoming requests from LGTM Enterprise, create issues in a specified Github repository and pass an appropriate response back to LGTM. We will implement our example for python 3.5+ and use two 3rd-party modules, `Flask` (a micro web framework) and `requests` (a user-friendly HTTP library). Both are extremely widely used and can be easily installed using `pip`. +```bash +pip install flask +pip install requests +``` + +## Basic Flask app + +For production-ready deployment it is important to consider both robustness and security, but in this tutorial we will focus on a minimal functioning implementation, demonstrating how to process the data from LGTM and integrate this with an example issue tracker. Similarly, we will avoid validation and error handling in this basic tutorial, and just assume for now that all the requests to the webhook are valid. + +The following is a basic 'hello world'-esque Flask app, which accepts POST requests and echoes the incoming JSON back to the sender. + +```python +from flask import Flask, request + +app = Flask(__name__) + +@app.route('/', methods=["POST"]) +def issues_webhook(): + return request.data, 200 + +if __name__ == "__main__": + app.run() +``` +Using the built-in WSGI development server that comes with Flask, this application can be run by simply executing the python file. +```bash +python flask_testing.py +``` +When successfully exectued, a service will be operating on localhost:5000 that will echo all POSTed JSON. + +## Format of webhook request + +All requests from LGTM to the specified webhook endpoint are of HTTP method POST, and they fall into three categories. +- `create` +- `close` +- `reopen` + +### Creating a new ticket + +A full example of a `create` request payload is given below. + +```json +{ + "transition": "create", + "project": { + "id": 1000001, + "url-identifier": "Git/example_user/example_repo", + "name": "example_user/example_repo", + "url": "http://lgtm.com/projects/Git/example_user/example_repo" + }, + "alert": { + "file": "/example.py", + "message": "Import of \"re\" is not used.\n", + "url": "http://lgtm.com/issues/1000001/python/8cdXzW+PyA3qiHBbWFomoMGtiIE=", + "query": { + "name": "Unused import", + "url": "http://lgtm.com/rules/1000678" + } + } +} +``` +With Flask, the payload of an incoming request can be accessed using the following utility function. +```python +json_dict = request.get_json() +``` +The Github API expects JSON with fields `title`, `body` and `labels`, and the body of the ticket can be formatted as markdown. For our example application, the following function takes `alert` and `project` from the LGTM payload and creates a dictionary that can be JSON serialized and then sent on to the correct Github endpoint. In this case we choose to just apply the single default label `LGTM` to all tickets. + +```python +def get_issue_dict(alert, project): + + title = "%s (%s)" % (alert["query"]["name"], project["name"]) + + lines = [] + lines.append("[%s](%s)" % (alert["query"]["name"], alert["query"]["url"])) + lines.append("") + lines.append("In %s:" % alert["file"]) + lines.append("> " + "\n> ".join(alert["message"].split("\n"))) + lines.append("[View alert on LGTM](%s)" % alert["url"]) + + return {"title": title, "body": "\n".join(lines), "labels": ["LGTM"]} +``` + +To interact with the Github API we use the `requests` module, and define the following details for the target Github repository, pulling the access token from an environment variable. +```python +URL = 'https://github.com/api/v3/repos/user/repo/issues' +HEADERS = {'content-type': 'application/json', + 'Authorization': 'Bearer %s' % os.getenv("GIT_ACCESS_TOKEN") + } +``` + +LGTM expects a 2XX HTTP response of the form shown below, where the `issue_id` provided will be stored and used in future requests to change the state of the ticket. +```json +{ + "issue-id": external_issue_id +} +``` + +Putting all of this together, in order to handle incoming `create` requests our example app becomes... + +```python +import os +from flask import Flask, request, jsonify +import requests + +app = Flask(__name__) + +URL = 'https://github.com/api/v3/repos/user/repo/issues' +HEADERS = {'content-type': 'application/json', + 'Authorization': 'Bearer %s' % os.getenv("GIT_ACCESS_TOKEN") + } + +def get_issue_dict(alert, project): + + title = "%s (%s)" % (alert["query"]["name"], project["name"]) + + lines = [] + lines.append("[%s](%s)" % (alert["query"]["name"], alert["query"]["url"])) + lines.append("") + lines.append("In %s:" % alert["file"]) + lines.append("> " + "\n> ".join(alert["message"].split("\n"))) + lines.append("[View alert on LGTM](%s)" % alert["url"]) + + return {"title": title, "body": "\n".join(lines), "labels": ["LGTM"]} + +@app.route('/', methods=["POST"]) +def issues_webhook(): + + json_dict = request.get_json() + + transition = json_dict.get('transition') + + if transition == 'create': + + data = get_issue_dict(json_dict.get('alert'), json_dict.get('project')) + + r = requests.post(URL, json=data, headers=HEADERS) + + issue_id = r.json()['number'] + + return jsonify({'issue-id': issue_id}), r.status_code + +if __name__ == "__main__": + app.run() +``` + +### Changing an existing ticket + +When closing an existing ticket, LGTM will send a request of the form... + +```json +{ + "issue-id": external_issue_id, + "transition": "close" +} +``` +When reopening a ticket, the request will be of the form... +```json +{ + "issue-id": external_issue_id, + "transition": "reopen" +} +``` +The Github API expects state to be specified as either `open` or `close`, so we first make sure that our terminology matches that expected by Github, and then send a simple `PATCH` request to the appropriate resource endpoint. +```python +if transition == 'create': + ######## +else: + issue_id = json_dict.get('issue-id') + + if transition == 'reopen': + transition = 'open' + + r = requests.patch(URL + '/' + issue_id, + json={"state": transition}, + headers=HEADERS) + + return jsonify({'issue-id': issue_id}), r.status_code +``` + +### Authorization + +When setting up the issue tracker integration a secret key is automatically generated, and this is used to crytographically sign all outgoing requests. These are signed in the same way as callbacks for the PR integrations, as already detailed elsewhere in [verify-callback-signature documentaition](https://lgtm.com/help/lgtm/api/run-code-review#verify-callback-signature). Verification of the incoming requests can therefore be easily achieved as follows. + +```python +import hmac + +KEY = os.getenv("LGTM_SECRET", '').encode('utf-8') + +digest = hmac.new(KEY, request.data, "sha1").hexdigest() +signature = request.headers.get('X-LGTM-Signature', "not-provided") + +if not hmac.compare_digest(signature, digest): + return jsonify({'message': "Unauthorized"}), 401 +``` + +## Full example + +Finally, putting all these piece together we have the following example Flask app, which will handle webhook requests from the LGTM issue tracker integration, and create tickets in the issue tracker of a specified Github repository. + +```python +import os +from flask import Flask, request, jsonify +import requests +import hmac + +app = Flask(__name__) + +URL = 'https://github.com/api/v3/repos/user/repo/issues' +HEADERS = {'content-type': 'application/json', + 'Authorization': 'Bearer %s' % os.getenv("GIT_ACCESS_TOKEN")} +KEY = os.getenv("LGTM_SECRET", '').encode('utf-8') + +def get_issue_dict(alert, project): + + title = "%s (%s)" % (alert["query"]["name"], project["name"]) + + lines = [] + lines.append("[%s](%s)" % (alert["query"]["name"], alert["query"]["url"])) + lines.append("") + lines.append("In %s:" % alert["file"]) + lines.append("> " + "\n> ".join(alert["message"].split("\n"))) + lines.append("[View alert on LGTM](%s)" % alert["url"]) + + return {"title": title, "body": "\n".join(lines), "labels": ["LGTM"]} + +@app.route('/', methods=["POST"]) +def issues_webhook(): + + digest = hmac.new(KEY, request.data, "sha1").hexdigest() + signature = request.headers.get('X-LGTM-Signature', "not-provided") + + if not hmac.compare_digest(signature, digest): + return jsonify({'message': "Unauthorized"}), 401 + + json_dict = request.get_json() + + transition = json_dict.get('transition') + + if transition == 'create': + + data = get_issue_dict(json_dict.get('alert'), json_dict.get('project')) + + r = requests.post(URL, json=data, headers=HEADERS) + + issue_id = r.json()['number'] + + return jsonify({'issue-id': issue_id}), r.status_code + + else: + + issue_id = json_dict.get('issue-id') + + if transition == 'reopen': + transition = 'open' + + r = requests.patch(URL + '/' + issue_id, + json={"state": transition}, + headers=HEADERS) + + return jsonify({'issue-id': issue_id}), r.status_code + + +if __name__ == "__main__": + app.run() +```