First Upload

This commit is contained in:
2024-10-19 18:23:55 +00:00
commit 9db52c11c3
11339 changed files with 1479286 additions and 0 deletions

147
vendor/enlightn/enlightn/CHANGELOG.md vendored Normal file
View File

@@ -0,0 +1,147 @@
# Release Notes
## [Unreleased](https://github.com/enlightn/enlightn/compare/v1.22.1...master)
## [v1.22.1 (2021-05-03)](https://github.com/enlightn/enlightn/compare/v1.22.0...v1.22.1)
### Fixed
- Add accept json header for invalid credential warnings ([#64](https://github.com/enlightn/enlightn/pull/64))
## [v1.22.0 (2021-04-15)](https://github.com/enlightn/enlightn/compare/v1.21.0...v1.22.0)
### Added
- Add report metadata for web UI ([#63](https://github.com/enlightn/enlightn/pull/63))
## [v1.21.0 (2021-03-23)](https://github.com/enlightn/enlightn/compare/v1.20.0...v1.21.0)
### Added
- Add new analyzer to check for custom error pages ([#60](https://github.com/enlightn/enlightn/pull/60))
## [v1.20.0 (2021-03-19)](https://github.com/enlightn/enlightn/compare/v1.19.0...v1.20.0)
### Added
- Add analyzer to check Composer autoloader optimization ([#58](https://github.com/enlightn/enlightn/pull/58))
## [v1.19.0 (2021-03-16)](https://github.com/enlightn/enlightn/compare/v1.18.0...v1.19.0)
### Added
- Add ability to display exception stack trace of analyzers ([#57](https://github.com/enlightn/enlightn/pull/57))
## [v1.18.0 (2021-03-11)](https://github.com/enlightn/enlightn/compare/v1.17.0...v1.18.0)
### Changed
- Ignore HTTP errors for checking headers ([#56](https://github.com/enlightn/enlightn/pull/56))
## [v1.17.0 (2021-03-09)](https://github.com/enlightn/enlightn/compare/v1.16.0...v1.17.0)
### Added
- Add analyzer to detect publicly accessible env file ([#55](https://github.com/enlightn/enlightn/pull/55))
## [v1.16.0 (2021-02-28)](https://github.com/enlightn/enlightn/compare/v1.15.1...v1.16.0)
### Added
- Say hello to the Enlightn Github bot ([#53](https://github.com/enlightn/enlightn/pull/53))
## [v1.15.1 (2021-02-21)](https://github.com/enlightn/enlightn/compare/v1.15.0...v1.15.1)
### Fixed
- Fix trusted proxies bug ([#49](https://github.com/enlightn/enlightn/pull/49))
## [v1.15.0 (2021-02-16)](https://github.com/enlightn/enlightn/compare/v1.14.0...v1.15.0)
### Added
- Add WTFPL to license whitelist ([#46](https://github.com/enlightn/enlightn/pull/46))
## [v1.14.0 (2021-02-14)](https://github.com/enlightn/enlightn/compare/v1.13.0...v1.14.0)
### Added
- Add ability to define preload code ([#43](https://github.com/enlightn/enlightn/pull/43))
### Fixed
- Fix view caching analyzer condition ([#42](https://github.com/enlightn/enlightn/pull/42))
## [v1.13.0 (2021-02-10)](https://github.com/enlightn/enlightn/compare/v1.12.0...v1.13.0)
### Changed
- Relax class property check for mixed objects ([#39](https://github.com/enlightn/enlightn/pull/39))
## [v1.12.0 (2021-02-10)](https://github.com/enlightn/enlightn/compare/v1.11.0...v1.12.0)
### Added
- Allow Larastan version 0.7 ([#38](https://github.com/enlightn/enlightn/pull/38))
## [v1.11.0 (2021-02-07)](https://github.com/enlightn/enlightn/compare/v1.10.0...v1.11.0)
### Added
- Add ability to ignore errors and establish a baseline ([#36](https://github.com/enlightn/enlightn/pull/36))
## [v1.10.0 (2021-02-04)](https://github.com/enlightn/enlightn/compare/v1.9.0...v1.10.0)
### Added
- Add details to analyzer fail messages ([#32](https://github.com/enlightn/enlightn/pull/32))
## [v1.9.0 (2021-02-03)](https://github.com/enlightn/enlightn/compare/v1.8.0...v1.9.0)
### Added
- Add support for CI mode ([#29](https://github.com/enlightn/enlightn/pull/29))
## [v1.8.0 (2021-02-01)](https://github.com/enlightn/enlightn/compare/v1.7.1...v1.8.0)
### Added
- Make improvements to static analysis ([#26](https://github.com/enlightn/enlightn/pull/26))
## [v1.7.1 (2021-01-29)](https://github.com/enlightn/enlightn/compare/v1.7...v1.7.1)
### Fixed
- Fix percentage calculations ([#22](https://github.com/enlightn/enlightn/pull/22))
### Added
- Faster tests by adding paratest and remove un-needed services ([#20](https://github.com/enlightn/enlightn/pull/20))
## [v1.7 (2021-01-27)](https://github.com/enlightn/enlightn/compare/v1.6...v1.7)
### Added
- Add analyzer to detect syntax errors ([#19](https://github.com/enlightn/enlightn/pull/19))
- Support custom categories ([#18](https://github.com/enlightn/enlightn/pull/18))
## [v1.6 (2021-01-26)](https://github.com/enlightn/enlightn/compare/v1.5...v1.6)
### Fixed
- Fix crash when there is a syntax error in one of the app files ([#17](https://github.com/enlightn/enlightn/pull/17))
## [v1.5 (2021-01-26)](https://github.com/enlightn/enlightn/compare/v1.4...v1.5)
### Added
- Add CC0 and Unlicense to list of whitelisted licenses ([#15](https://github.com/enlightn/enlightn/pull/15))
- Add option to show all files in the Enlightn command ([#16](https://github.com/enlightn/enlightn/pull/16))
## [v1.4 (2021-01-22)](https://github.com/enlightn/enlightn/compare/v1.3...v1.4)
### Added
- Add ability to exclude analyzers from reporting for CI/CD ([#12](https://github.com/enlightn/enlightn/pull/12))
### Fixed
- Add function check for opcache_get_configuration so it gracefully fails ([#10](https://github.com/enlightn/enlightn/pull/10))
- Fix logo for white terminals ([#11](https://github.com/enlightn/enlightn/pull/11))
## [v1.3 (2021-01-22)](https://github.com/enlightn/enlightn/compare/v1.2...v1.3)
### Added
- Add trinary maybe logic for PHPStan ([#9](https://github.com/enlightn/enlightn/pull/9))
## [v1.2 (2021-01-21)](https://github.com/enlightn/enlightn/compare/v1.1...v1.2)
### Changed
- Improved detection of HTTPS only apps ([#8](https://github.com/enlightn/enlightn/pull/8))
## [v1.1 (2021-01-20)](https://github.com/enlightn/enlightn/compare/v1.0...v1.1)
### Added
- Failing mode for CI ([#3](https://github.com/enlightn/enlightn/pull/3))
### Changed
- Skip XSS analyzer in local ([#6](https://github.com/enlightn/enlightn/pull/6))
- Replace SensioLabs security checker with Enlightn's own security checker ([#5](https://github.com/enlightn/enlightn/pull/5))
### Fixed
- Fix analyzer percentage computation ([#7](https://github.com/enlightn/enlightn/pull/7))

159
vendor/enlightn/enlightn/LICENSE.md vendored Normal file
View File

@@ -0,0 +1,159 @@
Copyright (c) Enlightn Software, Paras Malhotra
### GNU LESSER GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc.
<https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies of this
license document, but changing it is not allowed.
This version of the GNU Lesser General Public License incorporates the
terms and conditions of version 3 of the GNU General Public License,
supplemented by the additional permissions listed below.
#### 0. Additional Definitions.
As used herein, "this License" refers to version 3 of the GNU Lesser
General Public License, and the "GNU GPL" refers to version 3 of the
GNU General Public License.
"The Library" refers to a covered work governed by this License, other
than an Application or a Combined Work as defined below.
An "Application" is any work that makes use of an interface provided
by the Library, but which is not otherwise based on the Library.
Defining a subclass of a class defined by the Library is deemed a mode
of using an interface provided by the Library.
A "Combined Work" is a work produced by combining or linking an
Application with the Library. The particular version of the Library
with which the Combined Work was made is also called the "Linked
Version".
The "Minimal Corresponding Source" for a Combined Work means the
Corresponding Source for the Combined Work, excluding any source code
for portions of the Combined Work that, considered in isolation, are
based on the Application, and not on the Linked Version.
The "Corresponding Application Code" for a Combined Work means the
object code and/or source code for the Application, including any data
and utility programs needed for reproducing the Combined Work from the
Application, but excluding the System Libraries of the Combined Work.
#### 1. Exception to Section 3 of the GNU GPL.
You may convey a covered work under sections 3 and 4 of this License
without being bound by section 3 of the GNU GPL.
#### 2. Conveying Modified Versions.
If you modify a copy of the Library, and, in your modifications, a
facility refers to a function or data to be supplied by an Application
that uses the facility (other than as an argument passed when the
facility is invoked), then you may convey a copy of the modified
version:
- a) under this License, provided that you make a good faith effort
to ensure that, in the event an Application does not supply the
function or data, the facility still operates, and performs
whatever part of its purpose remains meaningful, or
- b) under the GNU GPL, with none of the additional permissions of
this License applicable to that copy.
#### 3. Object Code Incorporating Material from Library Header Files.
The object code form of an Application may incorporate material from a
header file that is part of the Library. You may convey such object
code under terms of your choice, provided that, if the incorporated
material is not limited to numerical parameters, data structure
layouts and accessors, or small macros, inline functions and templates
(ten or fewer lines in length), you do both of the following:
- a) Give prominent notice with each copy of the object code that
the Library is used in it and that the Library and its use are
covered by this License.
- b) Accompany the object code with a copy of the GNU GPL and this
license document.
#### 4. Combined Works.
You may convey a Combined Work under terms of your choice that, taken
together, effectively do not restrict modification of the portions of
the Library contained in the Combined Work and reverse engineering for
debugging such modifications, if you also do each of the following:
- a) Give prominent notice with each copy of the Combined Work that
the Library is used in it and that the Library and its use are
covered by this License.
- b) Accompany the Combined Work with a copy of the GNU GPL and this
license document.
- c) For a Combined Work that displays copyright notices during
execution, include the copyright notice for the Library among
these notices, as well as a reference directing the user to the
copies of the GNU GPL and this license document.
- d) Do one of the following:
- 0) Convey the Minimal Corresponding Source under the terms of
this License, and the Corresponding Application Code in a form
suitable for, and under terms that permit, the user to
recombine or relink the Application with a modified version of
the Linked Version to produce a modified Combined Work, in the
manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.
- 1) Use a suitable shared library mechanism for linking with
the Library. A suitable mechanism is one that (a) uses at run
time a copy of the Library already present on the user's
computer system, and (b) will operate properly with a modified
version of the Library that is interface-compatible with the
Linked Version.
- e) Provide Installation Information, but only if you would
otherwise be required to provide such information under section 6
of the GNU GPL, and only to the extent that such information is
necessary to install and execute a modified version of the
Combined Work produced by recombining or relinking the Application
with a modified version of the Linked Version. (If you use option
4d0, the Installation Information must accompany the Minimal
Corresponding Source and Corresponding Application Code. If you
use option 4d1, you must provide the Installation Information in
the manner specified by section 6 of the GNU GPL for conveying
Corresponding Source.)
#### 5. Combined Libraries.
You may place library facilities that are a work based on the Library
side by side in a single library together with other library
facilities that are not Applications and are not covered by this
License, and convey such a combined library under terms of your
choice, if you do both of the following:
- a) Accompany the combined library with a copy of the same work
based on the Library, uncombined with any other library
facilities, conveyed under the terms of this License.
- b) Give prominent notice with the combined library that part of it
is a work based on the Library, and explaining where to find the
accompanying uncombined form of the same work.
#### 6. Revised Versions of the GNU Lesser General Public License.
The Free Software Foundation may publish revised and/or new versions
of the GNU Lesser General Public License from time to time. Such new
versions will be similar in spirit to the present version, but may
differ in detail to address new problems or concerns.
Each version is given a distinguishing version number. If the Library
as you received it specifies that a certain numbered version of the
GNU Lesser General Public License "or any later version" applies to
it, you have the option of following the terms and conditions either
of that published version or of any later version published by the
Free Software Foundation. If the Library as you received it does not
specify a version number of the GNU Lesser General Public License, you
may choose any version of the GNU Lesser General Public License ever
published by the Free Software Foundation.
If the Library as you received it specifies that a proxy can decide
whether future versions of the GNU Lesser General Public License shall
apply, that proxy's public statement of acceptance of any version is
permanent authorization for you to choose that version for the
Library.

239
vendor/enlightn/enlightn/README.md vendored Normal file
View File

@@ -0,0 +1,239 @@
<h1 align="center">Enlightn</h1>
![tests](https://github.com/enlightn/enlightn/workflows/tests/badge.svg?branch=master)
[![LGPLv3 Licensed](https://img.shields.io/badge/license-LGPLv3-brightgreen.svg?style=flat-square)](LICENSE.md)
[![Latest Stable Version](https://poser.pugx.org/enlightn/enlightn/v/stable?format=flat-square)](https://packagist.org/packages/enlightn/enlightn)
[![Total Downloads](https://img.shields.io/packagist/dt/enlightn/enlightn.svg?style=flat-square)](https://packagist.org/packages/enlightn/enlightn)
[![Twitter Follow](https://img.shields.io/twitter/follow/Enlightn_app?label=Follow&style=social)](https://twitter.com/Enlightn_app)
<h2 align="center">A Laravel Tool To Boost Your App's Performance &amp; Security</h2>
![Enlightn](https://cdn.laravel-enlightn.com/images/mockups/enlightn_terminal128.png)
## Introduction
Think of Enlightn as your performance and security consultant. Enlightn will "review" your code and server configurations, and give you actionable recommendations on improving performance, security and reliability!
The Enlightn OSS (open source software) version has 66 automated checks that scan your application code, web server configurations and routes to identify performance bottlenecks, possible security vulnerabilities and code reliability issues.
Enlightn Pro (commercial) is available for purchase on the [Enlightn website](https://www.laravel-enlightn.com/) and has an additional 64 automated checks (total of **131 checks**).
### Performance Checks (37 Automated Checks including 19 Enlightn Pro Checks)
- 🚀 Performance Quick Wins (In-Built In Laravel): Route caching, config caching, etc.
- ⏳ Performance Bottleneck Identification: Middleware bloat, identification of slow, duplicate and N+1 queries, etc.
- 🍽️ Serving Assets: Minification, cache headers, CDN and compression headers.
- 🎛️ Infrastructure Tuning: Opcache, cache hit ratio, unix sockets for single server setups, etc.
- 🛸 Choosing The Right Driver: Choosing the right session, queue and cache drivers for your app.
- 🏆 Good Practices: Separate Redis databases for locks, dont install dev dependencies in production, etc.
### Security Checks (49 Automated Checks including 28 Enlightn Pro Checks)
- :lock: Basic Security: Turn off app debug in production, app key, CSRF protection, login throttling, hash strength, etc.
- :cookie: Cookie Security and Session Management: Cookie encryption, secure cookie attributes, session timeouts, etc.
- :black_joker: Mass Assignment: Detection of mass assignment vulnerabilities, unguarded models, etc.
- :radioactive: SQL Injection Attacks: Detection of raw SQL injection, column name SQL injection, validation rule injection, etc.
- :scroll: Security Headers: XSS, HSTS, clickjacking and MIME protection headers.
- :file_folder: Unrestricted File Uploads and DOS Attacks: Detection of directory traversal, storage DOS, unrestricted file uploads, etc.
- :syringe: Injection and Phishing Attacks: Detection of command injection, host injection, object injection, open redirection, etc.
- :package: Dependency Management: Backend and frontend vulnerability scanning, stable and up-to-date dependency checks, licensing, etc.
### Reliability Checks (45 Automated Checks including 17 Enlightn Pro Checks)
- 🧐 Code Reliability and Bug Detection: Invalid function calls, method calls, offsets, imports, return statements, syntax errors, invalid model relations, etc.
- :muscle: Health Checks: Health checks for cache, DB, directory permissions, migrations, disk space, symlinks, Redis, etc.
- :gear: Detecting Misconfigurations: Cache prefix, queue timeouts, failed job timeouts, Horizon provisioning plans, eviction policy, etc.
- :ghost: Dead Routes and Dead Code: Detection of dead routes and dead/unreachable code.
- :medal_sports: Good Practices: Cache busting, Composer scripts, env variables, avoiding globals and superglobals, etc.
## Documentation
Each of the 131 checks available are well documented. You can find the complete documentation [here](https://www.laravel-enlightn.com/docs/getting-started/installation.html).
## Compatibility Matrix
| Enlightn | Laravel | Larastan | PHPStan |
|:---------|:---------|:---------|:-----------|
| 1.x | 6.x-9.x | 0.6x-1.x | 0.12x-1.1x |
| 2.x | 9.x-11.x | 2.x | 1.4x+ |
Note: The same compatibility matrix applies for Enlightn Pro versions.
## Installing Enlightn OSS
You may install Enlightn into your project using the Composer package manager:
```bash
composer require enlightn/enlightn
```
After installing Enlightn, you may publish its assets using the vendor:publish Artisan command:
```bash
php artisan vendor:publish --tag=enlightn
```
Note: If you need to install Enlightn Pro, visit the documentation on the Enlightn website [here](https://www.laravel-enlightn.com/docs/getting-started/installation.html#installing-enlightn-pro).
## Running Enlightn
After installing Enlightn, simply run the `enlightn` Artisan command to run Enlightn:
```bash
php artisan enlightn
```
You may add the `--report` flag, if you wish to view your reports in the [Enlightn Web UI](https://www.laravel-enlightn.com/docs/getting-started/web-ui.html) besides the terminal:
```bash
php artisan enlightn --report
```
If you wish to run specific analyzer classes, you may specify them as optional arguments:
```bash
php artisan enlightn Enlightn\\Enlightn\\Analyzers\\Security\\CSRFAnalyzer Enlightn\\EnlightnPro\\Analyzers\\Security\\DirectoryTraversalAnalyzer
```
Note that the class names should be fully qualified and escaped with double slashes as above.
## Recommended to Run In Production
If you want to get the full Enlightn experience, it is recommended that you at least run Enlightn once in production. This is because several of Enlightn's checks are environment specific. So they may only be triggered when your app environment is production.
In case you don't want to run on production, you can simulate a production environment by setting your APP_ENV to production, setting up services and config as close to production as possible and running your production deployment script locally. Then run the Enlightn Artisan command.
## View Detailed Error Messages
By default, the `enlightn` Artisan command highlights the file paths, associated line numbers and a message for each failed check. If you wish to display detailed error messages for each line, you may use the `--details` option:
```bash
php artisan enlightn --details
```
## Usage in CI Environments
If you wish to integrate Enlightn with your CI, you can simply trigger the `--ci` option when running Enlightn in your CI/CD tool:
```bash
php artisan enlightn --ci
```
You may add the `--report` flag if you wish to view your CI reports in the [Enlightn Web UI](https://www.laravel-enlightn.com/docs/getting-started/web-ui.html). Remember to add your project credentials to your `config/enlightn.php` file as explained [here](https://www.laravel-enlightn.com/docs/getting-started/web-ui.html#how-to-get-access-free).
```bash
php artisan enlightn --ci --report
```
Enlightn pre-configures which analyzers can be run in CI mode for you. So, the above command excludes analyzers that need a full setup to run (e.g. analyzers using dynamic analysis).
For more information on CI integration, refer the [Enlightn documentation](https://www.laravel-enlightn.com/docs/getting-started/usage.html#usage-in-ci-environments).
## Establishing a Baseline
Sometimes, especially in CI environments, you may want to declare the currently reported list of errors as the "baseline". This means that the current errors will not be reported in subsequent runs and only new errors will be flagged.
To generate the baseline automatically, you may run the `enlightn:baseline` Artisan command:
```bash
php artisan enlightn:baseline
```
If you wish to run this command in CI mode, you can use the `--ci` option:
```bash
php artisan enlightn:baseline --ci
```
For more information on establishing a baseline, refer [the docs](https://www.laravel-enlightn.com/docs/getting-started/usage.html#establishing-a-baseline).
## Web UI
Enlightn offers a beautiful Web UI dashboard where you can view your Enlightn reports triggered from your CI or scheduled command runs.
![Enlightn Web UI Dashboard](https://cdn.laravel-enlightn.com/images/webui_report.png)
The web UI is free for all users and includes the following:
1. Statistics on pass percentages (overall and by category).
2. All failed checks along with code snippets related to the checks (if any).
3. Metrics on number of new and resolved issues (compared with the most recent report running on the same app URL, environment and project).
To get access to the Web UI, all you need to do is signup for free on the Enlightn website and follow the instructions mentioned [here](https://www.laravel-enlightn.com/docs/getting-started/web-ui.html#how-to-get-access-free).
## Scheduling Enlightn Runs
Besides integrating Enlightn with your CI/CD tool, it's a good practice to schedule an Enlightn run on a regular frequency (such as daily or weekly) like so:
```php
// In your app/Console/Kernel.php file:
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
$schedule->command('enlightn --report')->runInBackground()->daily()->at('01:00');
}
```
This will allow you to monitor Enlightn's dynamic analysis checks, which are typically excluded from CI. The reports can be viewed on the Enlightn [Web UI](https://www.laravel-enlightn.com/docs/getting-started/web-ui.html).
## GitHub Bot Integration
Enlightn offers a GitHub bot that can prepare a report highlighting failed checks and also add review comments for pull requests on the lines of code that introduce new issues.
![Enlightn GitHub Bot Review Comments](https://cdn.laravel-enlightn.com/images/github-bot.png)
To integrate with the Enlightn GitHub bot, refer [the docs](https://www.laravel-enlightn.com/docs/getting-started/github-bot.html).
## Failed Checks
All checks that fail will include a description of why they failed along with the associated lines of code (if applicable) and a link to the documentation for the specific check.
<img src="https://www.laravel-enlightn.com/docs/images/queue-timeout.png" width="70%" alt="Enlightn Failed Check" />
## Report Card
Finally, after all the checks have run, the `enlightn` Artisan command will output a report card, which contains information on how many and what percentage of checks passed, failed or were skipped.
<img src="https://www.laravel-enlightn.com/docs/images/report_card.png" width="70%" alt="Enlightn Report Card" />
The checks indicated as "Not Applicable" were not applicable to your specific application and were skipped. For instance, the CSRF analyzer is not applicable for stateless applications.
The checks reported under the "Error" row indicate the analyzers that failed with exceptions during the analysis. Normally, this should not happen but if it does, the associated error message will be displayed and may have something to do with your application.
## How Frequently Should I Run Enlightn?
A good practice would be to run Enlightn every time you are deploying code or pushing a new release. It is recommended to integrate Enlightn with your CI/CD tool so that it is triggered for every push or new release.
Besides the automated CI checks, you should also run Enlightn on a regular frequency using a scheduled console command as described above. This will allow you to monitor the dynamic analysis checks, which are typically excluded from CI.
## Featured On
[<img src="https://yt3.googleusercontent.com/WBigdQgh_nxly0BisZKNA5Ej2P1cBdjcBT7YmJlrGbjOy7KY8h9AOljW2XkH46lvgk6RhZUbJg=s900-c-k-c0x00ffffff-no-rj" height="100" alt="Laravel News" />](https://laravel-news.com/enlightn) &nbsp;&nbsp;&nbsp; [<img src="https://owasp.org/www-policy/branding-assets/OWASP-Combination-mark-r.png" height="100" alt="OWASP" />](https://cheatsheetseries.owasp.org/cheatsheets/Laravel_Cheat_Sheet.html) &nbsp;&nbsp;&nbsp; [<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/e/ee/NIST_logo.svg/1280px-NIST_logo.svg.png" height="80" alt="NIST" />](https://www.nist.gov/itl/ssd/software-quality-group/source-code-security-analyzers)
## Flagship OSS Projects Using Enlightn
[<img src="https://laravel.io/images/laravelio.png" height="60" alt="Laravel.io" />](https://github.com/laravelio/laravel.io) &nbsp;&nbsp;&nbsp; [<img src="https://akaunting.com/public/images/logo.png" height="80" alt="Akaunting" />](https://github.com/akaunting/akaunting)
## OS Compatibility
Only MacOS and Linux systems are supported for Enlightn. Windows is currently not supported.
## Contribution Guide
Thank you for considering contributing to Enlightn! The contribution guide can be found [here](https://www.laravel-enlightn.com/docs/getting-started/contribution-guide.html).
## Support Policy
Our support policy can be found in the [Enlightn documentation](https://www.laravel-enlightn.com/docs/getting-started/support.html).
## License
The Enlightn OSS (on this GitHub repo) is licensed under the [LGPL v3 (or later) license](LICENSE.md).
Enlightn Pro is licensed under a [commercial license](https://www.laravel-enlightn.com/license-agreement).

7
vendor/enlightn/enlightn/SECURITY.md vendored Normal file
View File

@@ -0,0 +1,7 @@
# Security Policy
**PLEASE DON'T DISCLOSE SECURITY-RELATED ISSUES PUBLICLY, [SEE BELOW](#reporting-a-vulnerability).**
## Reporting a Vulnerability
If you discover a security vulnerability within Enlightn, please send an email to Paras Malhotra at paras@laravel-enlightn.com. All security vulnerabilities will be promptly addressed.

83
vendor/enlightn/enlightn/composer.json vendored Normal file
View File

@@ -0,0 +1,83 @@
{
"name": "enlightn/enlightn",
"description": "Enlightn - Your performance & security consultant, an artisan command away.",
"type": "library",
"keywords": [
"laravel",
"package",
"static analysis",
"dynamic analysis",
"static analyzer",
"dynamic analyzer",
"security",
"performance",
"audit",
"analysis tool"
],
"homepage": "https://www.laravel-enlightn.com/",
"license": "LGPL-3.0-or-later",
"authors": [
{
"name": "Paras Malhotra",
"email": "paras@laravel-enlightn.com"
},
{
"name": "Miguel Piedrafita",
"email": "soy@miguelpiedrafita.com"
},
{
"name": "Lars Klopstra",
"email": "lars@flowframe.nl"
}
],
"support": {
"issues": "https://github.com/enlightn/enlightn/issues",
"docs": "https://www.laravel-enlightn.com/docs/"
},
"require": {
"php": "^8.0",
"ext-json": "*",
"enlightn/security-checker": "^1.1|^2.0",
"guzzlehttp/guzzle": "^7.0",
"larastan/larastan": "^2.0",
"laravel/framework": "^9.0|^10.0|^11.0",
"nikic/php-parser": "^4.0|^5.0",
"phpstan/phpstan": ">=1.10.48",
"phpstan/phpstan-deprecation-rules": "^1.1",
"symfony/finder": "^4.0|^5.0|^6.0|^7.0"
},
"require-dev": {
"barryvdh/laravel-ide-helper": "^2.8|^3.0",
"brianium/paratest": "^6.1|^7.0",
"friendsofphp/php-cs-fixer": "^2.18|^3.0",
"mockery/mockery": "^1.3",
"orchestra/testbench": "^7.0|^8.0|^9.0",
"phpunit/phpunit": "^9.0|^10.0",
"predis/predis": "*"
},
"autoload": {
"psr-4": {
"Enlightn\\Enlightn\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Enlightn\\Enlightn\\Tests\\": "tests"
}
},
"scripts": {
"test": "vendor/bin/paratest --colors"
},
"config": {
"sort-packages": true
},
"extra": {
"laravel": {
"providers": [
"Enlightn\\Enlightn\\EnlightnServiceProvider"
]
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,186 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Enlightn Analyzer Classes
|--------------------------------------------------------------------------
|
| The following array lists the "analyzer" classes that will be registered
| with Enlightn. These analyzers run an analysis on the application via
| various methods such as static analysis. Feel free to customize it.
|
*/
'analyzers' => ['*'],
// If you wish to skip running some analyzers, list the classes in the array below.
'exclude_analyzers' => [],
// If you wish to skip running some analyzers in CI mode, list the classes below.
'ci_mode_exclude_analyzers' => [],
/*
|--------------------------------------------------------------------------
| Enlightn Analyzer Paths
|--------------------------------------------------------------------------
|
| The following array lists the "analyzer" paths that will be searched
| recursively to find analyzer classes. This option will only be used
| if the analyzers option above is set to the asterisk wildcard. The
| key is the base namespace to resolve the class name.
|
*/
'analyzer_paths' => [
'Enlightn\\Enlightn\\Analyzers' => base_path('vendor/enlightn/enlightn/src/Analyzers'),
'Enlightn\\EnlightnPro\\Analyzers' => base_path('vendor/enlightn/enlightnpro/src/Analyzers'),
],
/*
|--------------------------------------------------------------------------
| Enlightn Base Path
|--------------------------------------------------------------------------
|
| The following array lists the directories that will be scanned for
| application specific code. By default, we are scanning your app
| folder, migrations folder and the seeders folder.
|
*/
'base_path' => [
app_path(),
database_path('migrations'),
database_path('seeders'),
],
/*
|--------------------------------------------------------------------------
| Environment Specific Analyzers
|--------------------------------------------------------------------------
|
| There are some analyzers that are meant to be run for specific environments.
| The options below specify whether we should skip environment specific
| analyzers if the environment does not match.
|
*/
'skip_env_specific' => env('ENLIGHTN_SKIP_ENVIRONMENT_SPECIFIC', false),
/*
|--------------------------------------------------------------------------
| Guest URL
|--------------------------------------------------------------------------
|
| Specify any guest url or path (preferably your app's login url) here. This
| would be used by Enlightn to inspect your application HTTP headers.
| Example: '/login'.
|
*/
'guest_url' => null,
/*
|--------------------------------------------------------------------------
| Exclusions From Reporting
|--------------------------------------------------------------------------
|
| Specify the analyzer classes that you wish to exclude from reporting. This
| means that if any of these analyzers fail, they will not be counted
| towards the exit status of the Enlightn command. This is useful
| if you wish to run the command in your CI/CD pipeline.
| Example: [\Enlightn\Enlightn\Analyzers\Security\XSSAnalyzer::class].
|
*/
'dont_report' => [],
/*
|--------------------------------------------------------------------------
| Ignoring Errors
|--------------------------------------------------------------------------
|
| Use this config option to ignore specific errors. The key of this array
| would be the analyzer class and the value would be an associative
| array with path and details. Run php artisan enlightn:baseline
| to auto-generate this. Patterns are supported in details.
|
*/
'ignore_errors' => [],
/*
|--------------------------------------------------------------------------
| Analyzer Configurations
|--------------------------------------------------------------------------
|
| The following configuration options pertain to individual analyzers.
| These are recommended options but feel free to customize them based
| on your application needs.
|
*/
'license_whitelist' => [
'Apache-2.0', 'Apache2', 'BSD-2-Clause', 'BSD-3-Clause', 'LGPL-2.1-only', 'LGPL-2.1',
'LGPL-2.1-or-later', 'LGPL-3.0', 'LGPL-3.0-only', 'LGPL-3.0-or-later', 'MIT', 'ISC',
'CC0-1.0', 'Unlicense', 'WTFPL',
],
/*
|--------------------------------------------------------------------------
| Credentials
|--------------------------------------------------------------------------
|
| The following credentials are used to share your Enlightn report with
| the Enlightn Github Bot. This allows the bot to compile the report
| and add review comments on your pull requests.
|
*/
'credentials' => [
'username' => env('ENLIGHTN_USERNAME'),
'api_token' => env('ENLIGHTN_API_TOKEN'),
],
// Set this value to your Github repo for integrating with the Enlightn Github Bot
// Format: "myorg/myrepo" like "laravel/framework".
'github_repo' => env('ENLIGHTN_GITHUB_REPO'),
// Set to true to restrict the max number of files displayed in the enlightn
// command for each check. Set to false to display all files.
'compact_lines' => true,
// List your commercial packages (licensed by you) below, so that they are not
// flagged by the License Analyzer.
'commercial_packages' => [
'enlightn/enlightnpro',
],
'allowed_permissions' => [
base_path() => '775',
app_path() => '775',
resource_path() => '775',
storage_path() => '775',
public_path() => '775',
config_path() => '775',
database_path() => '775',
base_path('routes') => '775',
app()->bootstrapPath() => '775',
app()->bootstrapPath('cache') => '775',
app()->bootstrapPath('app.php') => '664',
base_path('artisan') => '775',
public_path('index.php') => '664',
public_path('server.php') => '664',
],
'writable_directories' => [
storage_path(),
app()->bootstrapPath('cache'),
],
/*
|--------------------------------------------------------------------------
| PHPStan Runtime configurations
|--------------------------------------------------------------------------
|
| This setting allows us to pass through memory limits from artisan to phpstan.
| using `php -d memory_limit=1G artisan enlightn`.
*/
'phpstan' => [
'--error-format' => 'json',
'--no-progress' => true,
'--memory-limit' => ini_get('memory_limit'),
],
];

116
vendor/enlightn/enlightn/phpstan.neon vendored Normal file
View File

@@ -0,0 +1,116 @@
includes:
- %rootDir%/../../larastan/larastan/extension.neon
- %rootDir%/../../phpstan/phpstan-deprecation-rules/rules.neon
parameters:
customRulesetUsed: true
noUnnecessaryCollectionCall: true
noUnnecessaryCollectionCallOnly: []
noUnnecessaryCollectionCallExcept: []
checkModelProperties: false
reportMagicMethods: false
reportMagicProperties: false
checkFunctionNameCase: false
checkPhpDocMethodSignatures: false
checkExplicitMixedMissingReturn: false
checkDynamicProperties: false
checkPhpDocMissingReturn: false
checkMaybeUndefinedVariables: true
cliArgumentsVariablesRegistered: true
checkNullables: false
checkThisOnly: false
checkUnionTypes: true
checkExplicitMixed: false
checkMissingOverrideMethodAttribute: false
featureToggles:
genericPrototypeMessage: true
logicalXor: true
finalByPhpDoc: false
stubFiles:
- stubs/Request.stub
rules:
- Enlightn\Enlightn\PHPStan\FillableForeignKeyModelRule
- Enlightn\Enlightn\PHPStan\MassAssignmentModelInstanceRule
- Enlightn\Enlightn\PHPStan\MassAssignmentModelStaticRule
- Enlightn\Enlightn\PHPStan\MassAssignmentBuilderInstanceRule
- PHPStan\Rules\Arrays\DeadForeachRule
- PHPStan\Rules\Arrays\IterableInForeachRule
- PHPStan\Rules\Arrays\OffsetAccessAssignmentRule
- PHPStan\Rules\Arrays\OffsetAccessAssignOpRule
- PHPStan\Rules\Arrays\OffsetAccessValueAssignmentRule
- PHPStan\Rules\Classes\ClassConstantRule
- PHPStan\Rules\DeadCode\UnreachableStatementRule
- PHPStan\Rules\DeadCode\UnusedPrivateConstantRule
- PHPStan\Rules\DeadCode\UnusedPrivateMethodRule
- PHPStan\Rules\Functions\CallToFunctionParametersRule
- PHPStan\Rules\Functions\PrintfParametersRule
- PHPStan\Rules\Functions\ReturnTypeRule
- PHPStan\Rules\Methods\CallMethodsRule
- PHPStan\Rules\Methods\CallStaticMethodsRule
- PHPStan\Rules\Methods\ReturnTypeRule
- PHPStan\Rules\Variables\UnsetRule
services:
-
class: PHPStan\Rules\Functions\CallToNonExistentFunctionRule
tags:
- phpstan.rules.rule
arguments:
checkFunctionNameCase: %checkFunctionNameCase%
-
class: PHPStan\Rules\Methods\OverridingMethodRule
arguments:
checkPhpDocMethodSignatures: %checkPhpDocMethodSignatures%
genericPrototypeMessage: %featureToggles.genericPrototypeMessage%
finalByPhpDoc: %featureToggles.finalByPhpDoc%
checkMissingOverrideMethodAttribute: %checkMissingOverrideMethodAttribute%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\DeadCode\NoopRule
arguments:
logicalXor: %featureToggles.logicalXor%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Missing\MissingReturnRule
arguments:
checkExplicitMixedMissingReturn: %checkExplicitMixedMissingReturn%
checkPhpDocMissingReturn: %checkPhpDocMissingReturn%
tags:
- phpstan.rules.rule
-
class: PHPStan\Rules\Namespaces\ExistingNamesInUseRule
tags:
- phpstan.rules.rule
arguments:
checkFunctionNameCase: %checkFunctionNameCase%
-
class: PHPStan\Rules\Namespaces\ExistingNamesInGroupUseRule
tags:
- phpstan.rules.rule
arguments:
checkFunctionNameCase: %checkFunctionNameCase%
-
class: PHPStan\Rules\Properties\AccessPropertiesRule
tags:
- phpstan.rules.rule
arguments:
reportMagicProperties: %reportMagicProperties%
checkDynamicProperties: %checkDynamicProperties%
-
class: PHPStan\Rules\Variables\DefinedVariableRule
arguments:
cliArgumentsVariablesRegistered: %cliArgumentsVariablesRegistered%
checkMaybeUndefinedVariables: %checkMaybeUndefinedVariables%
tags:
- phpstan.rules.rule
-
class: Enlightn\Enlightn\PHPStan\RequestDataTypeNodeResolverExtension
tags:
- phpstan.phpDoc.typeNodeResolverExtension
-
class: Enlightn\Enlightn\PHPStan\RequestArrayDataTypeNodeResolverExtension
tags:
- phpstan.phpDoc.typeNodeResolverExtension

View File

View File

@@ -0,0 +1,344 @@
<?php
namespace Enlightn\Enlightn\Analyzers;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Str;
use Throwable;
abstract class Analyzer
{
const SEVERITY_CRITICAL = 'critical';
const SEVERITY_MAJOR = 'major';
const SEVERITY_MINOR = 'minor';
const SEVERITY_INFO = 'info';
/**
* The base URL of the Enlightn documentation.
*
* @var string
*/
const DOCS_URL = 'https://www.laravel-enlightn.com/docs';
/**
* The category of the analyzer.
*
* @var string|null
*/
public $category = null;
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = null;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = null;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = null;
/**
* The error message describing the analyzer insights.
*
* @var string|null
*/
public $errorMessage = null;
/**
* The application paths and associated line numbers to flag.
*
* @var array
*/
public $traces = [];
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = true;
/**
* The exception thrown during the analysis.
*
* @var array
*/
protected $exceptionMessage = null;
/**
* The stack trace of the exception thrown during the analysis.
*
* @var array
*/
protected $stackTrace = null;
/**
* Determine whether the analyzer passed.
*
* @var bool
*/
protected $passed = true;
/**
* Determine whether the analyzer was skipped.
*
* @var bool
*/
protected $skipped = false;
/**
* Run the analyzer.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function run(Application $app)
{
if (method_exists($this, 'skip') && $this->skip()) {
$this->markSkipped();
return;
}
$method = method_exists($this, 'handle') ? 'handle' : '__invoke';
$app->call([$this, $method]);
}
/**
* Get the error message pertaining to the analysis.
*
* @return string
*/
public function getErrorMessage()
{
return method_exists($this, 'errorMessage') ? $this->errorMessage() : $this->errorMessage;
}
/**
* Add an associated path and line number trace.
*
* @param string $path
* @param int $lineNumber
* @param string|null $details
* @return $this
*/
public function addTrace(string $path, $lineNumber = 0, $details = null)
{
if ($lineNumber == 0) {
return $this->markFailed();
}
if ($this->isIgnoredError($path, $details)) {
return $this;
}
if (! in_array($trace = new Trace($path, $lineNumber, $details), $this->traces)) {
$this->traces[] = $trace;
}
return $this->markFailed();
}
/**
* Push a trace to the traces array.
*
* @param \Enlightn\Enlightn\Analyzers\Trace $trace
* @return $this
*/
public function pushTrace(Trace $trace)
{
if ($this->isIgnoredError($trace->path, $trace->details)) {
return $this;
}
if (! in_array($trace, $this->traces)) {
$this->traces[] = $trace;
}
return $this->markFailed();
}
/**
* Record an exception that was thrown during the analysis.
*
* @param \Throwable $e
* @return $this
*/
public function recordException(Throwable $e)
{
$this->exceptionMessage = $e->getMessage();
$this->stackTrace = $e->getTraceAsString();
return $this->markSkipped();
}
/**
* Set an exception message for the analyzer.
*
* @param string $message
* @return $this
*/
public function setExceptionMessage(string $message)
{
$this->exceptionMessage = $message;
return $this->markSkipped();
}
/**
* Mark the analyzer as failed.
*
* @return $this
*/
public function markFailed()
{
$this->passed = false;
return $this;
}
/**
* Mark the analyzer as skipped.
*
* @return $this
*/
public function markSkipped()
{
$this->skipped = true;
return $this;
}
/**
* Get the analyzer information.
*
* @return array
*/
public function getInfo()
{
return [
'title' => $this->title,
'category' => $this->category,
'severity' => $this->severity,
'timeToFix' => $this->timeToFix,
'status' => $this->getStatus(),
'exception' => $this->exceptionMessage,
'error' => ($this->getStatus() == 'failed') ? $this->getErrorMessage() : null,
'traces' => $this->traces,
'docsUrl' => $this->getDocsUrl(),
'reportable' => ! in_array(static::class, config('enlightn.dont_report', [])),
'class' => static::class,
'stackTrace' => $this->stackTrace,
];
}
/**
* Get the analyzer status.
*
* @return string
*/
public function getStatus()
{
if ($this->runFailed()) {
return 'error';
} elseif ($this->skipped()) {
return 'skipped';
} else {
return $this->passed() ? 'passed' : 'failed';
}
}
/**
* Determine whether the analyzer passed.
*
* @return bool
*/
public function passed()
{
return $this->passed;
}
/**
* Determine whether the analyzer was skipped.
*
* @return bool
*/
public function skipped()
{
return $this->skipped;
}
/**
* Determine whether the analyzer run failed with an exception.
*
* @return bool
*/
public function runFailed()
{
return ! is_null($this->exceptionMessage);
}
/**
* Get the documentation URL for this analyzer.
*
* @return bool
*/
public function getDocsUrl()
{
$page = $this->docsPageName ??
Str::kebab(
str_replace(
['CSRF', 'SQL', 'HSTS', 'NPlusOne', 'XSS', 'PHP'],
['Csrf', 'Sql', 'Hsts', 'Nplusone', 'Xss', 'Php'],
class_basename(get_class($this))
)
);
return self::DOCS_URL.'/'.strtolower($this->category).'/'.$page.'.html';
}
/**
* Determine whether the analyzer should skip if the environment is local.
*
* @return bool
*/
public function isLocalAndShouldSkip()
{
return config('app.env') === 'local' && config('enlightn.skip_env_specific', false);
}
/**
* Determine whether the error should be ignored.
*
* @param string $path
* @param string|null $details
* @return bool
*/
public function isIgnoredError(string $path, $details)
{
$ignoredErrors = config('enlightn.ignore_errors', []);
if (! isset($ignoredErrors[static::class])) {
return false;
}
return collect($ignoredErrors[static::class])
->contains(function ($info) use ($path, $details) {
return ($info['path'] == $path || base_path(trim($info['path'], '/')) == $path) &&
Str::is($info['details'], $details);
});
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Concerns;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
trait AnalyzesHeaders
{
/**
* The Guzzle client instance.
*
* @var \GuzzleHttp\Client
*/
protected $client;
/**
* Set the Guzzle client.
*
* @param \GuzzleHttp\Client $client
*/
public function setClient(Client $client)
{
$this->client = $client;
}
/**
* Determine if the header(s) exist on the URL.
*
* @param string|null $url
* @param string|array $headers
* @param array $options
* @return bool
*/
protected function headerExistsOnUrl($url, $headers, $options = [])
{
if (is_null($url)) {
// If we can't find the route, we cannot perform this check.
return false;
}
try {
$response = $this->client->get($url, array_merge(['http_errors' => false], $options));
return collect($headers)->contains(function ($header) use ($response) {
return $response->hasHeader($header);
});
} catch (GuzzleException $e) {
return false;
}
}
/**
* Get the headers on the URL.
*
* @param string|null $url
* @param string $header
* @param array $options
* @return array
*/
protected function getHeadersOnUrl($url, string $header, $options = [])
{
if (is_null($url)) {
// If we can't find the route, we cannot perform this check.
return [];
}
try {
$response = $this->client->get($url, array_merge(['http_errors' => false], $options));
return $response->getHeader($header);
} catch (GuzzleException $e) {
return [];
}
}
}

View File

@@ -0,0 +1,216 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Concerns;
use Closure;
use Exception;
use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Str;
use ReflectionClass;
trait AnalyzesMiddleware
{
/**
* The router instance.
*
* @var \Illuminate\Routing\Router
*/
protected $router;
/**
* The HTTP kernel instance.
*
* @var \Illuminate\Contracts\Http\Kernel
*/
protected $kernel;
/**
* Determine if the application uses the provided middleware.
*
* @param string $middlewareClass
* @return bool
* @throws \ReflectionException
*/
protected function appUsesMiddleware(string $middlewareClass)
{
return $this->getAllMiddleware()->contains(function ($middleware) use ($middlewareClass) {
return $middleware === $middlewareClass
|| (class_exists($middlewareClass) && is_subclass_of($middleware, $middlewareClass));
});
}
/**
* Compile a list of all middlewares used by the application.
*
* @return \Illuminate\Support\Collection
* @throws \ReflectionException
*/
protected function getAllMiddleware()
{
return $this->getAllRouteMiddleware()->merge($this->getGlobalMiddleware());
}
/**
* Compile a list of all route middlewares used by the application.
*
* @return \Illuminate\Support\Collection
*/
protected function getAllRouteMiddleware()
{
return collect($this->router->getRoutes())->map(function ($route) {
return $this->getMiddleware($route);
})->flatten()->unique();
}
/**
* Snatch the global middleware from the kernel instance using reflection witchcraft.
* Kids, don't try this at home.
*
* No way around this as there's no method to get or register global middleware.
*
* @return array
* @throws \ReflectionException
*/
protected function getGlobalMiddleware()
{
$mirror = new ReflectionClass($this->kernel);
$property = $mirror->getProperty('middleware');
$property->setAccessible(true);
return collect((array) $property->getValue($this->kernel))->map(function ($middleware) {
// To get the middleware class names, we must separate the parameters.
return Str::before($middleware, ':');
})->toArray();
}
/**
* Determine if the application uses the provided global HTTP middleware.
*
* @param string $middlewareClass
* @return bool
* @throws \ReflectionException
*/
protected function appUsesGlobalMiddleware(string $middlewareClass)
{
return collect($this->getGlobalMiddleware())->contains(function ($middleware) use ($middlewareClass) {
return $middleware === $middlewareClass
|| (class_exists($middlewareClass) && is_subclass_of($middleware, $middlewareClass));
});
}
/**
* Determine if the application uses the provided middleware.
*
* @param \Illuminate\Routing\Route $route
* @param string $middlewareClass
* @return bool
*/
protected function routeUsesMiddleware($route, string $middlewareClass)
{
return collect($this->getMiddleware($route))->contains(function ($middleware) use ($middlewareClass) {
return $middleware === $middlewareClass
|| (class_exists($middlewareClass) && is_subclass_of($middleware, $middlewareClass));
});
}
/**
* Get the middleware for a route.
*
* @param \Illuminate\Routing\Route $route
*
* @return array
*/
protected function getMiddleware($route)
{
return collect($this->router->gatherRouteMiddleware($route))->map(function ($middleware) {
return $middleware instanceof Closure ? 'Closure' : $middleware;
})->map(function ($middleware) {
// To get the middleware class names, we must separate the parameters.
return Str::before($middleware, ':');
})->toArray();
}
/**
* Determine if the application uses the provided middleware class (by basename).
*
* @param \Illuminate\Routing\Route $route
* @param string $basenameMiddlewareClass
* @return bool
*/
protected function routeUsesBasenameMiddleware($route, string $basenameMiddlewareClass)
{
return collect($this->getBasenameMiddlewareClasses($route))
->contains(function ($middleware) use ($basenameMiddlewareClass) {
return $middleware === $basenameMiddlewareClass;
});
}
/**
* Get the basename of the middleware classes for a route.
*
* @param \Illuminate\Routing\Route $route
*
* @return array
*/
protected function getBasenameMiddlewareClasses($route)
{
return collect($this->getMiddleware($route))->map(function ($middleware) {
return class_basename($middleware);
})->toArray();
}
/**
* Determine if the app is stateless.
*
* @return bool
* @throws \ReflectionException
*/
protected function appIsStateless()
{
// If the app doesn't start sessions, it is stateless
return ! $this->appUsesMiddleware(StartSession::class);
}
/**
* Determine if the app uses cookies.
*
* @return bool
* @throws \ReflectionException
*/
protected function appUsesCookies()
{
return $this->appUsesMiddleware(AddQueuedCookiesToResponse::class);
}
/**
* Find the login route url. Returns null if not found.
*
* @return string|null
*/
protected function findLoginRoute()
{
// First, we check to see if a guest path is provided. If yes, we return the corresponding URL.
if (! is_null($guestPath = config('enlightn.guest_url'))) {
return url($guestPath);
}
// Here we just return the login named route. By default, Laravel uses
// the named route "login" for all its auth scaffolding packages.
try {
return route('login');
} catch (Exception $e) {
// Next, we try to search for the first route that has the guest middleware.
$route = collect($this->router->getRoutes())->filter(function ($route) {
return $this->routeUsesBasenameMiddleware($route, 'RedirectIfAuthenticated');
})->first();
if (! is_null($route)) {
return url($route->uri());
} else {
// If all else fails, default to the root URL.
return url('/');
}
}
}
}

View File

@@ -0,0 +1,20 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Concerns;
use Illuminate\Support\Str;
trait DetectsHttps
{
/**
* Determine if the application is an HTTPS only app.
*
* @return bool
*/
protected function appIsHttpsOnly()
{
// We assume here that if the app URL points to a URL with the https protocol or if the secure attribute
// is set to true (as the default for all app cookies), then the app is an HTTPS only app.
return Str::contains(config('app.url'), 'https://') || config('session.secure') == true;
}
}

View File

@@ -0,0 +1,21 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Concerns;
trait DetectsRedis
{
/**
* Determine if the application uses any Redis services.
*
* @return bool
*/
protected function appUsesRedis()
{
// We assume here that if the cache, session, broadcasting or queue is powered by Redis,
// then the application is using Redis.
return (config('cache.stores.'.config('cache.default').'.driver') === 'redis'
|| config('broadcasting.connections.'.config('broadcasting.default').'.driver') === 'redis'
|| config('session.driver') === 'redis'
|| config('queue.connections.'.config('queue.default').'.driver') === 'redis');
}
}

View File

@@ -0,0 +1,44 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Concerns;
use Enlightn\Enlightn\Inspection\Inspector;
use Enlightn\Enlightn\Inspection\QueryBuilder;
trait InspectsCode
{
/**
* Inspect the code, record the errors in the inspector and determine if the code passes the analysis.
*
* @param \Enlightn\Enlightn\Inspection\Inspector $inspector
* @param \Enlightn\Enlightn\Inspection\QueryBuilder $builder
* @return bool
*/
protected function passesCodeInspection(Inspector $inspector, QueryBuilder $builder)
{
$inspector->inspect($builder);
return $inspector->passed();
}
/**
* Inspect the code and record error traces if the inspection fails.
*
* @param \Enlightn\Enlightn\Inspection\Inspector $inspector
* @param \Enlightn\Enlightn\Inspection\QueryBuilder $builder
*/
protected function inspectCode(Inspector $inspector, QueryBuilder $builder)
{
if (! $this->passesCodeInspection($inspector, $builder)) {
collect($inspector->getLastErrors())->each(function ($trace) {
$this->pushTrace($trace);
});
// Although adding traces would also mark it as failed, but there may be no traces
// at all, yet should still be failed.
if (empty($inspector->getLastErrors())) {
$this->markFailed();
}
}
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Concerns;
use Enlightn\Enlightn\FileParser;
use Illuminate\Support\Str;
trait ParsesConfigurationFiles
{
/**
* Record a configuration error.
*
* @param string $config
* @param string $key
* @param array $after
* @return void
*/
public function recordError($config, $key, $after = [])
{
if (file_exists(
$filePath = config(
'enlightn.config_path',
config_path()
).DIRECTORY_SEPARATOR."{$config}.php"
)) {
$key = Str::before($key, '.');
$this->addTrace(
$filePath,
(int) FileParser::getLineNumber($filePath, ["'{$key}'", '"'.$key.'"'], $after)
);
} else {
$this->markFailed();
}
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Concerns;
use Enlightn\Enlightn\Analyzers\Trace;
use Enlightn\Enlightn\PHPStan;
trait ParsesPHPStanAnalysis
{
/**
* Parse the analysis and add traces for the errors.
*
* @param \Enlightn\Enlightn\PHPStan $phpStan
* @param string|array $search
*/
protected function parsePHPStanAnalysis(PHPStan $phpStan, $search)
{
collect($phpStan->parseAnalysis($search))->each(function (Trace $trace) {
$this->addTrace($trace->path, $trace->lineNumber, $trace->details);
});
}
/**
* Parse the analysis and add traces for the errors.
*
* @param \Enlightn\Enlightn\PHPStan $phpStan
* @param string|array $pattern
*/
protected function matchPHPStanAnalysis(PHPStan $phpStan, $pattern)
{
collect($phpStan->match($pattern))->each(function (Trace $trace) {
$this->addTrace($trace->path, $trace->lineNumber, $trace->details);
});
}
/**
* Parse the analysis and add traces for the errors.
*
* @param \Enlightn\Enlightn\PHPStan $phpStan
* @param string|array $pattern
*/
protected function pregMatchPHPStanAnalysis(PHPStan $phpStan, $pattern)
{
collect($phpStan->pregMatch($pattern))->each(function (Trace $trace) {
$this->addTrace($trace->path, $trace->lineNumber, $trace->details);
});
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Enlightn\Enlightn\Analyzers;
use SplFileObject;
class File
{
/**
* @var \SplFileObject
*/
private $file;
public function __construct(string $path)
{
$this->file = new SplFileObject($path);
}
/**
* @return int
*/
public function numberOfLines()
{
$this->file->seek(PHP_INT_MAX);
return $this->file->key() + 1;
}
/**
* @param int|null $lineNumber
* @return string
*/
public function getLine(int $lineNumber = null)
{
if (is_null($lineNumber)) {
return $this->getNextLine();
}
$this->file->seek($lineNumber - 1);
return $this->file->current();
}
/**
* @return string
*/
public function getNextLine()
{
$this->file->next();
return $this->file->current();
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Illuminate\Database\Query\Builder;
class AutoloaderOptimizationAnalyzer extends PerformanceAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application has the Composer autoloader optimization configured in production.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your Composer autoloader is not optimized while your application is in a non-local environment. "
."You should optimize the autoloader for improved performance.";
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
/** @var \Composer\Autoload\ClassLoader $loader */
$loader = require base_path('vendor/autoload.php');
if (! $loader->isClassMapAuthoritative() && ! isset($loader->getClassMap()[Builder::class])) {
// We assume here that if composer autoloader isn't optimized using the --classmap-authoritative flag
// and does not have the classmap loaded for the Builder class, then it is not optimized because
// PSR-4 rules should be converted into classmap rules with the -o flag.
$this->markFailed();
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
*/
public function skip()
{
// Skip the analyzer if it's a local env.
return config('app.env') === 'local';
}
}

View File

@@ -0,0 +1,127 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesConfigurationFiles;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class CacheDriverAnalyzer extends PerformanceAnalyzer
{
use ParsesConfigurationFiles;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'A proper cache driver is configured.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 60;
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
$defaultStore = $config->get('cache.default');
$driver = ucfirst($config->get("cache.stores.{$defaultStore}.driver", "null"));
if (method_exists($this, "assess{$driver}Driver")) {
if (! $this->{"assess{$driver}Driver"}($config)) {
$this->recordError('cache', 'default');
}
}
}
/**
* Assess whether a proper cache driver is set.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return bool
*/
protected function assessNullDriver($config)
{
$this->errorMessage = "Your cache driver is set to null. This means that your app is not "
."using caching and all cache read operations will result in a miss. This setting is "
."only suitable for test environments in specific situations.";
return false;
}
/**
* Assess whether a proper cache driver is set.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return bool
*/
protected function assessArrayDriver($config)
{
$this->errorMessage = "Your cache driver is set to array. This means that your app is not "
."using caching and caches will not be persisted outside the running PHP process in any way. "
."This setting is only suitable for testing.";
return false;
}
/**
* Assess whether a proper cache driver is set.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return bool
*/
protected function assessFileDriver($config)
{
if ($config->get('app.env') === 'local') {
// file system cache is perfectly fine for local dev
return true;
}
$this->errorMessage = "Your cache driver is set to file in a non-local environment. "
."This means that your app uses the local filesystem for caching. This setting is "
."only suitable if your app is hosted on a single server setup. Even for single "
."server setups, a cache system such as Redis or Memcached are better suited "
."for performance (when using unix sockets) and more efficient eviction of expired "
."cache items.";
$this->severity = self::SEVERITY_MINOR;
return false;
}
/**
* Assess whether a proper cache driver is set.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return bool
*/
protected function assessDatabaseDriver($config)
{
if ($config->get('app.env') === 'local') {
// database cache is perfectly fine for local dev
return true;
}
$this->errorMessage = "Your cache driver is set to database in a non-local environment. "
."This setting is not suitable for production environments. Cache drivers such as "
."Redis or Memcached are much more robust and better suited for production.";
$this->severity = self::SEVERITY_MINOR;
return false;
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Analyzers\Concerns\AnalyzesHeaders;
use GuzzleHttp\Client;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
class CacheHeaderAnalyzer extends PerformanceAnalyzer
{
use AnalyzesHeaders;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application caches compiled assets for improved performance.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 15;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* The list of uncached assets.
*
* @var \Illuminate\Support\Collection
*/
protected $unCachedAssets;
/**
* Create a new analyzer instance.
*
* @return void
*/
public function __construct()
{
$this->client = new Client();
}
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application does not set appropriate cache headers on your compiled Laravel Mix assets. "
."To improve performance, it is recommended to set Cache Control headers on your Mix assets via "
."your web server configuration. Your uncached assets include: {$this->formatUncachedAssets()}.";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @return void
* @throws \Exception
*/
public function handle(Filesystem $files)
{
$manifest = json_decode($files->get(public_path('mix-manifest.json')), true);
$this->unCachedAssets = collect();
foreach ($manifest as $key => $value) {
if (is_string($value) && Str::contains($value, '?id=')
&& ! $this->headerExistsOnUrl((string) mix($key), 'Cache-Control')
&& ! $this->headerExistsOnUrl(asset($key), 'Cache-Control')) {
// We only take the cache busted (versioned) files as the others are presumably un-cacheable.
$this->unCachedAssets->push($key);
}
}
if ($this->unCachedAssets->count() > 0) {
$this->markFailed();
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
*/
public function skip()
{
// Skip the analyzer if it's a local env or if the application does not use Laravel Mix.
return $this->isLocalAndShouldSkip() || ! file_exists(public_path('mix-manifest.json'));
}
/**
* @return string
*/
protected function formatUncachedAssets()
{
return $this->unCachedAssets->map(function ($file) {
return "[{$file}]";
})->join(', ', ' and ');
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class CollectionCallAnalyzer extends PerformanceAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Aggregation is done at the database query level rather than at the Laravel Collection level.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 10;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application performs some aggregations at the Laravel Collection level instead of the database "
."query level. For example, a `Model::all()->count()` call can easily be replaced with a `Model::count()`. "
."Aggregations on collections result in heavy database queries and unneeded Collection loops. This should "
."be avoided for better application performance.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $phpStan
* @return void
*/
public function handle(PHPStan $phpStan)
{
$this->parsePHPStanAnalysis($phpStan, 'but could have been retrieved as a query');
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Foundation\CachesConfiguration;
class ConfigCachingAnalyzer extends PerformanceAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Application config caching is configured properly.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MINOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(Application $app, ConfigRepository $config)
{
if ($config->get('app.env') === 'local' && $app->configurationIsCached()) {
$this->errorMessage = "Your app config is cached in a local environment. "
."This is not recommended for development because as you change your config files, "
."the changes will not be reflected unless you clear the cache.";
$this->markFailed();
} elseif ($config->get('app.env') !== 'local' && ! $app->configurationIsCached()) {
$this->errorMessage = "Your app config is not cached in a non-local environment. "
."Config caching enables a performance improvement and it is recommended to "
."enable this in production.";
$this->markFailed();
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
*/
public function skip()
{
// Skip the analyzer if application does not implement the interface. Since the interface
// was only introduced in Laravel 7, we will have to check if the interface class exists.
return class_exists(CachesConfiguration::class) && ! (app() instanceof CachesConfiguration);
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesConfigurationFiles;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class DebugLogAnalyzer extends PerformanceAnalyzer
{
use ParsesConfigurationFiles;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application does not use the debug log level in production.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MINOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 1;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your app log level is set to debug while your application is in a non-local environment. "
."This is not recommended and may slow down your application.";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
if ($config->get('app.env') === 'local') {
return;
}
$defaultLogChannel = $config->get('logging.default');
if ($defaultLogChannel !== 'stack'
&& $config->get('logging.channels.'.$defaultLogChannel.'.level') === 'debug') {
$this->recordError('logging', 'level', ['channels', $defaultLogChannel]);
}
if ($defaultLogChannel === 'stack') {
foreach ($config->get('logging.channels.stack.channels') as $channel) {
if ($config->get('logging.channels.'.$channel.'.level') === 'debug') {
$this->recordError('logging', 'level', ['channels', 'ignore_exceptions', $channel]);
}
}
}
}
}

View File

@@ -0,0 +1,68 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Composer;
use Illuminate\Support\Str;
class DevDependencyAnalyzer extends PerformanceAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Dev dependencies are not installed in production.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 1;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application's dev dependencies are installed while your application is in a non-local environment. "
."This may slow down your application as dev dependencies such as Ignition are known to have memory "
."leaks and are automatically discovered if you have package discovery enabled.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\Composer $composer
* @return void
*/
public function handle(Composer $composer)
{
if (config('app.env') === 'local') {
return;
}
if (Str::contains($composer->installDryRun(['--no-dev']), 'Removing')) {
// If composer install --dry-run --no-dev results in removing a package, that means
// that the application has installed dev dependencies.
$this->markFailed();
}
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Analyzers\Concerns\InspectsCode;
use Enlightn\Enlightn\Inspection\Inspector;
use Enlightn\Enlightn\Inspection\QueryBuilder;
class EnvCallAnalyzer extends PerformanceAnalyzer
{
use InspectsCode;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application does not contain env function calls outside of your config files.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 10;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return 'Your application contains env function calls outside of your config files. You must ensure that '
.'you are calling the "env" function from within your configuration files. Once the configuration '
.'has been cached, the .env file will not be loaded and all calls to the "env" function would '
.'return null. This means that your code will not work when your configuration is cached.';
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\Inspection\Inspector $inspector
* @return void
*/
public function handle(Inspector $inspector)
{
$builder = (new QueryBuilder)->doesntHaveFunctionCall('env');
$this->inspectCode($inspector, $builder);
}
}

View File

@@ -0,0 +1,67 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class HorizonSuggestionAnalyzer extends PerformanceAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application uses Horizon when using the Redis queue driver.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MINOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 15;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application uses the Redis queue driver but does not use Laravel's first party package "
."called Horizon for queue management. Horizon not only offers a beautiful dashboard for queues "
."and job monitoring, it also offers configurable provisioning plans for queue workers, load "
."balancing strategies and memory management features. It is definitely recommended to install "
."the Horizon package for any Laravel application that uses Redis queues.";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
if (! class_exists(\Laravel\Horizon\Horizon::class)) {
$this->markFailed();
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
*/
public function skip()
{
// Skip the analyzer if application does not use the Redis queue driver.
return config('queue.connections.'.config('queue.default').'.driver') !== 'redis';
}
}

View File

@@ -0,0 +1,110 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Filesystem;
use Illuminate\Support\Str;
use Symfony\Component\Finder\Finder;
class MinificationAnalyzer extends PerformanceAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application minifies assets in production.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* A list of un-minified assets served by the application.
*
* @var string
*/
protected $unMinifiedAssets;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application does not minify all assets (js, css) while in a non-local environment. "
."Minification of assets can provide a significant performance boost for your application "
."and is recommended for production. Your un-minified assets include: {$this->unMinifiedAssets}.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\Filesystem $files
* @return void
*/
public function handle(Filesystem $files)
{
if (config('app.env') === 'local') {
return;
}
$this->unMinifiedAssets = collect($this->getFilesThatShouldBeMinified())->map(function ($fileInfo) {
return $fileInfo->getRealPath();
})->filter(function ($path) use ($files) {
// We assume here that any file with more than 10 lines is not minified. That should
// take care of the copyright notice (if any), sourcemap URL, etc. Case in point:
// Bootstrap minified css/js actually have 7 lines.
return $files->lines($path)->count() > 10;
})->map(function ($path) {
return Str::contains($path, base_path())
? ('['.trim(Str::after($path, base_path()), '/').']') : '['.$path.']';
})->join(', ', ' and ');
if (! empty($this->unMinifiedAssets)) {
$this->markFailed();
}
}
/**
* @return \Symfony\Component\Finder\Finder
*/
protected function getFilesThatShouldBeMinified()
{
// We assume that all assets are in the public directory. However, this can be configured
// using the "build_path" configuration option.
return (new Finder)->in(config('enlightn.build_path', public_path()))->name([
// This would automatically include files named like *.min.js as well.
'*.js', '*.css',
])->files();
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
*/
public function skip()
{
// Skip the analyzer if there are no files to be minified (e.g. API only apps).
return ($this->getFilesThatShouldBeMinified()->count() == 0);
}
}

View File

@@ -0,0 +1,87 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesConfigurationFiles;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class MysqlSingleServerAnalyzer extends PerformanceAnalyzer
{
use ParsesConfigurationFiles;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'MySQL is configured properly on single server setups.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 30;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "When MySQL is running on the same server as your app, it is recommended to use unix "
."sockets instead of TCP ports to improve performance by upto 50% (Percona benchmark).";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
$localConnections = collect($config->get('database.connections', []))
->filter(function ($conf) {
// filter the local connections that don't use sockets
return isset($conf['driver']) && $conf['driver'] === 'mysql'
&& isset($conf['host']) && $conf['host'] === '127.0.0.1'
&& (! isset($conf['unix_socket']) || empty($conf['unix_socket']));
});
if ($localConnections->count() > 0) {
// On same server setups, it is recommended to use unix sockets
$this->recordError('database', 'unix_socket');
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
*/
public function skip()
{
if ($this->isLocalAndShouldSkip()
|| config('database.connections.'.config('database.default').'.driver') !== 'mysql') {
return true;
}
return false;
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
class OpcacheAnalyzer extends PerformanceAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'OPcache is enabled.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 10;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "OPcache is currently disabled. OPcache can give your application a significant performance boost "
."and it is recommended to enable it in production.";
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
if (function_exists('opcache_get_configuration')
&& opcache_get_configuration()['directives']['opcache.enable'] ?? false) {
return;
}
$this->markFailed();
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Analyzers\Analyzer;
abstract class PerformanceAnalyzer extends Analyzer
{
/**
* The category of the analyzer.
*
* @var string|null
*/
public $category = 'Performance';
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesConfigurationFiles;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class QueueDriverAnalyzer extends PerformanceAnalyzer
{
use ParsesConfigurationFiles;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'A proper queue driver is configured.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 60;
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
$connection = $config->get('queue.default');
$driver = ucfirst($config->get("queue.connections.{$connection}.driver", "null"));
if (method_exists($this, "assess{$driver}Driver")) {
if (! $this->{"assess{$driver}Driver"}($config)) {
$this->recordError('queue', 'default');
}
}
}
/**
* Assess whether a proper queue driver is set.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return bool
*/
protected function assessNullDriver($config)
{
$this->errorMessage = "Your queue driver is set to null. This means that your app ignores "
."any jobs, mails, notifications or events sent to the queue. This can be very dangerous "
."and is only suitable for test environments in specific situations.";
return false;
}
/**
* Assess whether a proper queue driver is set.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return bool
*/
protected function assessSyncDriver($config)
{
$this->errorMessage = "Your queue driver is set to sync. This means that all jobs, mails, "
."notifications and event listeners will be processed immediately in a synchronous "
."manner. These time consuming tasks will slow down web requests and this driver is not "
."suitable for production environments. Even for local development, it is recommended to use "
."other drivers in order to accurately simulate production behaviour.";
return false;
}
/**
* Assess whether a proper queue driver is set.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return bool
*/
protected function assessDatabaseDriver($config)
{
if ($config->get('app.env') === 'local') {
// Database queue driver is perfectly fine for local dev
return true;
}
$this->errorMessage = "Your queue driver is set to database in a non-local environment. "
."The database queue driver is not suitable for production environments and is known "
."to have issues such as deadlocks and slowing down your database during peak queue "
."backlogs. It is strongly recommended to shift to Redis, SQS or Beanstalkd.";
$this->severity = self::SEVERITY_MINOR;
return false;
}
}

View File

@@ -0,0 +1,74 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Foundation\CachesRoutes;
class RouteCachingAnalyzer extends PerformanceAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Application route caching is configured properly.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(Application $app, ConfigRepository $config)
{
if ($config->get('app.env') === 'local' && $app->routesAreCached()) {
$this->errorMessage = "Your app routes are cached in a local environment. "
."This is not recommended for development because as you change your route files, "
."the changes will not be reflected unless you clear the cache.";
$this->markFailed();
} elseif ($config->get('app.env') !== 'local' && ! $app->routesAreCached()) {
$this->errorMessage = "Your app routes are not cached in a non-local environment. "
."Route caching enables a significant improvement of upto 5X and it is recommended to "
."enable this in production. Remember to add the Artisan route:cache command "
."to your deployment script so that every time you deploy, the cache is regenerated.";
$this->markFailed();
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
*/
public function skip()
{
// Skip the analyzer if application does not implement the interface
return class_exists(CachesRoutes::class) && ! (app() instanceof CachesRoutes);
}
}

View File

@@ -0,0 +1,161 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Analyzers\Concerns\AnalyzesMiddleware;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesConfigurationFiles;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Routing\Router;
class SessionDriverAnalyzer extends PerformanceAnalyzer
{
use ParsesConfigurationFiles;
use AnalyzesMiddleware;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'A proper session driver is configured.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 60;
/**
* Create a new analyzer instance.
*
* @param \Illuminate\Routing\Router $router
* @param \Illuminate\Contracts\Http\Kernel $kernel
* @return void
*/
public function __construct(Router $router, Kernel $kernel)
{
$this->router = $router;
$this->kernel = $kernel;
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
$driver = ucfirst($config->get('session.driver'));
if (method_exists($this, "assess{$driver}Driver")) {
if (! $this->{"assess{$driver}Driver"}($config)) {
// Record an error if the assessment failed.
$this->recordError('session', 'driver');
}
}
}
/**
* Assess whether a proper session driver is set.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return bool
*/
protected function assessNullDriver($config)
{
$this->errorMessage = "Your session driver is set to null while you have some routes that "
."use the session. This means that all session read operations will result in a miss. "
."This setting is only suitable for test environments in specific situations.";
return false;
}
/**
* Assess whether a proper session driver is set.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return bool
*/
protected function assessArrayDriver($config)
{
$this->errorMessage = "Your session driver is set to array while you have some routes that "
."use the session. This means that session data will not be persisted outside the running "
."PHP process in any way. This setting is only suitable for testing.";
return false;
}
/**
* Assess whether a proper session driver is set.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return bool
*/
protected function assessFileDriver($config)
{
if ($config->get('app.env') === 'local') {
// file system session is perfectly fine for local dev
return true;
}
$this->errorMessage = "Your session driver is set to file in a non-local environment "
."while you have some routes that use the session. This means that your app uses "
."the local filesystem for persisting session data. This setting is only "
."suitable if your app is hosted on a single server setup. Even for single "
."server setups, a session system such as Redis or Memcached are better suited "
."for performance (when using unix sockets) and more efficient eviction of expired "
."session items.";
$this->severity = self::SEVERITY_MINOR;
return false;
}
/**
* Assess whether a proper session driver is set.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return bool
*/
protected function assessCookieDriver($config)
{
if ($config->get('app.env') === 'local') {
// cookie sessions are perfectly fine for local dev
return true;
}
$this->errorMessage = "Your session driver is set to cookie in a non-local environment "
."while you have some routes that use the session. This means that your app uses "
."client-side cookies for persisting session data. This setting is not advisable as "
."cookies have a size limit of 4kB, are stored on the client-side, are temporary in "
."nature and may be susceptible to change on the client side if you aren't using "
."the EncryptCookies middleware. Consider changing your session driver to more robust "
."options such as database, Redis, Memcached or DynamoDB.";
$this->severity = self::SEVERITY_MINOR;
return false;
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
* @throws \ReflectionException
*/
public function skip()
{
// Skip this analyzer if the app is stateless and does not use sessions.
return $this->appIsStateless();
}
}

View File

@@ -0,0 +1,79 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Analyzers\Concerns\InspectsCode;
use Enlightn\Enlightn\Inspection\Inspector;
use Enlightn\Enlightn\Inspection\QueryBuilder;
use Illuminate\Support\Facades\Cache;
class SharedCacheLockAnalyzer extends PerformanceAnalyzer
{
use InspectsCode;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application does not use locks on your default cache store.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MINOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return 'Your application uses cache locks on your default cache store. This means that when '
.'your cache is cleared, your locks will also be cleared. Typically, this is not the '
.'intention when using locks for managing race conditions or concurrent processing. '
.'If you intend to persist locks despite cache clearing, it is recommended that '
.'you use cache locks on a separate store, which uses a connection and database that '
.'is not shared with your default cache store.';
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\Inspection\Inspector $inspector
* @return void
*/
public function handle(Inspector $inspector)
{
if (! is_null($lockConnection = config('cache.stores.'.config('cache.default').'.lock_connection'))
&& $lockConnection !== config('cache.stores.'.config('cache.default').'.connection')) {
// Laravel 8.20+ ships with an option to have a separate lock connection.
return;
}
$builder = (new QueryBuilder)->doesntHaveStaticCall(Cache::class, 'lock');
$this->inspectCode($inspector, $builder);
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
*/
public function skip()
{
// Skip this analyzer if the application does not use a Redis cache driver.
return config('cache.stores.'.config('cache.default').'.driver') !== 'redis';
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Enlightn\Enlightn\Analyzers\Concerns\AnalyzesMiddleware;
use Enlightn\Enlightn\Inspection\Reflector;
use Fideloper\Proxy\TrustProxies;
use Fruitcake\Cors\HandleCors;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Http\Middleware\HandleCors as L9HandleCors;
use Illuminate\Http\Middleware\TrustHosts;
use Illuminate\Http\Middleware\TrustProxies as L9TrustProxies;
use Illuminate\Routing\Router;
class UnusedGlobalMiddlewareAnalyzer extends PerformanceAnalyzer
{
use AnalyzesMiddleware;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application does not contain unused global HTTP middleware.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MINOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* A collection of unused middleware
*
* @var \Illuminate\Support\Collection
*/
protected $unusedMiddleware;
/**
* Create a new analyzer instance.
*
* @param \Illuminate\Routing\Router $router
* @param \Illuminate\Contracts\Http\Kernel $kernel
* @return void
*/
public function __construct(Router $router, Kernel $kernel)
{
$this->router = $router;
$this->kernel = $kernel;
}
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return 'Your application contains global middleware that is not currently being used. It is '
.'recommended to remove these middleware from your Kernel class to enhance performance '
.'slightly. Your unused middleware include: '.$this->formatUnusedMiddleware();
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
* @throws \ReflectionException
*/
public function handle(Application $app, ConfigRepository $config)
{
$this->unusedMiddleware = collect();
if ((class_exists(TrustProxies::class) && $this->appUsesGlobalMiddleware(TrustProxies::class)) || (class_exists(L9TrustProxies::class) && $this->appUsesGlobalMiddleware(L9TrustProxies::class))) {
$middlewareClass = collect($this->getGlobalMiddleware())->filter(function ($middleware) {
return $middleware === TrustProxies::class || is_subclass_of($middleware, TrustProxies::class) || $middleware === L9TrustProxies::class || is_subclass_of($middleware, L9TrustProxies::class);
})->first();
$middleware = $app->make($middlewareClass);
$proxies = Reflector::get($middleware, 'proxies');
if (empty($proxies) && is_null($config->get('trustedproxy.proxies'))) {
$this->unusedMiddleware->push(class_exists(L9TrustProxies::class) ? L9TrustProxies::class : TrustProxies::class);
if ($this->appUsesGlobalMiddleware(TrustHosts::class)) {
// Trusted hosts without trusted proxies is useless.
$this->unusedMiddleware->push(TrustHosts::class);
}
}
} elseif ($this->appUsesGlobalMiddleware(TrustHosts::class)) {
// Trusted hosts without trusted proxies is useless.
$this->unusedMiddleware->push(TrustHosts::class);
}
if (empty($config->get('cors.paths', [])) && ($this->appUsesGlobalMiddleware(HandleCors::class) || $this->appUsesGlobalMiddleware(L9HandleCors::class))) {
$this->unusedMiddleware->push(class_exists(L9HandleCors::class) ? L9HandleCors::class : HandleCors::class);
}
if ($this->unusedMiddleware->count() > 0) {
$this->markFailed();
}
}
/**
* @return string
*/
protected function formatUnusedMiddleware()
{
return $this->unusedMiddleware->map(function ($middlewareClass) {
return '['.class_basename($middlewareClass).']';
})->join(', ', ' and ');
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Performance;
use Illuminate\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
class ViewCachingAnalyzer extends PerformanceAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'View caching is configured properly.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MINOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Execute the analyzer.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @return void
*/
public function handle(Filesystem $files)
{
if (config('app.env') === 'local') {
return;
}
$viewCount = 0;
$this->paths()->each(function ($path) use (&$viewCount) {
$viewCount += ($this->bladeFilesIn([$path]))->count();
});
$path = config('view.compiled');
$compiledViewCount = count($files->glob("{$path}/*"));
if ($viewCount > $compiledViewCount) {
$this->errorMessage = "Your views are not cached in a non-local environment. "
."View caching enables a performance improvement and it is recommended to "
."enable this in production.";
$this->markFailed();
}
}
/**
* Get the Blade files in the given path.
*
* @param array $paths
* @return \Illuminate\Support\Collection
*/
protected function bladeFilesIn(array $paths)
{
return collect(
Finder::create()
->in($paths)
->exclude('vendor')
->name('*.blade.php')
->files()
);
}
/**
* Get all of the possible view paths.
*
* @return \Illuminate\Support\Collection
*/
protected function paths()
{
$finder = app('view')->getFinder();
return collect($finder->getPaths())->merge(
collect($finder->getHints())->flatten()
)->unique();
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesConfigurationFiles;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class CachePrefixAnalyzer extends ReliabilityAnalyzer
{
use ParsesConfigurationFiles;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Cache prefix is set to avoid collisions with other apps.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 1;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your cache prefix is too generic and may result in collisions with other apps "
."that share the same cache servers. In general, this should be fixed if you set "
."a non-generic app name in your .env file.";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
if (! ($prefix = $config->get('cache.prefix')) ||
$prefix == 'laravel_cache') {
$this->recordError('cache', 'prefix');
}
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Str;
use Throwable;
class CacheStatusAnalyzer extends ReliabilityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application cache is working.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* The current disk usage (in percentage).
*
* @var float
*/
protected $diskUsage;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application cache seems to be offline.";
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
$payload = Str::random(10);
try {
Cache::put('enlightn:check', $payload, 10);
if (Cache::get('enlightn:check') !== $payload) {
$this->markFailed();
}
} catch (Throwable $e) {
$this->markFailed();
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Composer;
use Illuminate\Support\Str;
class ComposerValidationAnalyzer extends ReliabilityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application's composer.json file is valid.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 10;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application's composer.json file is not valid. Run the composer validate command "
."to view more details.";
}
/**
* Execute the analyzer.
*
* @param Composer $composer
* @return void
*/
public function handle(Composer $composer)
{
if (! Str::contains($composer->runCommand(['validate']), 'is valid')) {
$this->markFailed();
}
}
}

View File

@@ -0,0 +1,106 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\AnalyzesMiddleware;
use Illuminate\Contracts\Debug\ExceptionHandler;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Routing\Router;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
class CustomErrorPageAnalyzer extends ReliabilityAnalyzer
{
use AnalyzesMiddleware;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application defines custom error page views.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MINOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 60;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Create a new analyzer instance.
*
* @param \Illuminate\Routing\Router $router
* @param \Illuminate\Contracts\Http\Kernel $kernel
* @return void
*/
public function __construct(Router $router, Kernel $kernel)
{
$this->router = $router;
$this->kernel = $kernel;
}
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application does not customize its error pages. This may hamper user experience and also exposes "
."your application to fingerprinting, which means potential attackers can identify Laravel as your framework.";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @return void
*/
public function handle(Filesystem $files)
{
$hasCustomErrorPages = collect(config('view.paths'))->contains(function ($viewPath, $_) use ($files) {
return $files->exists($viewPath.DIRECTORY_SEPARATOR.'errors'.DIRECTORY_SEPARATOR.'404.blade.php');
});
$hasCustomErrorNamespace = isset(app('view')->getFinder()->getHints()['errors']);
if ($hasCustomErrorPages || $hasCustomErrorNamespace) {
return;
}
// Throw a NotFoundHttpException and check if the rendered response matches the default 404 page (for apps like Inertia).
$response = app(ExceptionHandler::class)->render(app('request'), new NotFoundHttpException());
if ($response->getContent() === view('errors::404')->render()) {
$this->markFailed();
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
* @throws \ReflectionException
*/
public function skip()
{
// Skip this analyzer if the app is stateless. We assume here that if the app is stateless, it's probably not
// a web app and therefore, does not need to define any views.
return $this->appIsStateless();
}
}

View File

@@ -0,0 +1,80 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Illuminate\Support\Facades\DB;
use Throwable;
class DatabaseStatusAnalyzer extends ReliabilityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Database is accessible.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* The connections that are not accessible.
*
* @var string
*/
protected $failedConnections;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application's database connection(s) is/are not accessible: ".$this->failedConnections;
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
$databaseConnectionsToCheck = config('enlightn.database_connections', [
config('database.default'),
]);
$this->failedConnections = collect($databaseConnectionsToCheck)->filter()->reject(function ($connection) {
try {
DB::connection($connection)->getPdo();
return true;
} catch (Throwable $e) {
return false;
}
})->join(', ', ' and ');
if (! empty($this->failedConnections)) {
$this->markFailed();
}
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class DeadCodeAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application does not contain any dead or unreachable code.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MINOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 10;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application contains some dead or unreachable code. It is good practice to delete all unreachable "
."code so as to improve code readability.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $phpStan
* @return void
*/
public function handle(PHPStan $phpStan)
{
$this->parsePHPStanAnalysis($phpStan, [
'does not do anything', 'Unreachable statement', 'is unused', 'Empty array passed',
]);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class DeprecatedCodeAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not use any deprecated code.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MINOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 10;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to use deprecated code. This may include calls to deprecated methods / functions, accessing deprecated properties or using deprecated classes / interfaces / traits.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->pregMatchPHPStanAnalysis($PHPStan, '#\s* deprecated \s*#');
}
}

View File

@@ -0,0 +1,73 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Illuminate\Filesystem\Filesystem;
use Illuminate\Support\Str;
class DirectoryWritePermissionsAnalyzer extends ReliabilityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your storage and cache directories are writable.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* The directories that are not writable.
*
* @var string
*/
protected $failedDirs;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application's storage and cache directories are not writable. This can cause issues "
."with your Laravel installation. The directories that are not writable include: {$this->failedDirs}";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @return void
*/
public function handle(Filesystem $files)
{
$directoriesToCheck = config('enlightn.writable_directories', [
storage_path(),
app()->bootstrapPath('cache'),
]);
$this->failedDirs = collect($directoriesToCheck)->reject(function ($directory) use ($files) {
return $files->isWritable($directory);
})->map(function ($path) {
return Str::contains($path, base_path())
? ('['.trim(Str::after($path, base_path()), '/').']') : '['.$path.']';
})->join(', ', ' and ');
if (! empty($this->failedDirs)) {
$this->markFailed();
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Dotenv\Dotenv;
class EnvExampleAnalyzer extends ReliabilityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "All env variables used in your .env file are defined in your .env.example file.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MINOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 10;
/**
* The missing .env variables.
*
* @var \Illuminate\Support\Collection
*/
protected $missingEnvVariables;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application has some missing environment variables that are defined in your .env file "
."but missing in your .env.example file: ".$this->formatMissingEnvVariables();
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
if (method_exists(Dotenv::class, 'createImmutable')) {
$this->handleDotEnvV4();
return;
}
$examples = Dotenv::create(base_path(), '.env.example');
$actual = Dotenv::create(base_path(), '.env');
$examples->safeLoad();
$actual->safeLoad();
$this->missingEnvVariables = collect($actual->getEnvironmentVariableNames())
->diff($examples->getEnvironmentVariableNames());
if (! $this->missingEnvVariables->isEmpty()) {
$this->markFailed();
}
}
/**
* Execute the analyzer for DotEnv v4.
*/
protected function handleDotEnvV4()
{
$examples = Dotenv::createMutable(base_path(), '.env.example');
$actual = Dotenv::createMutable(base_path(), '.env');
$this->missingEnvVariables = collect($actual->safeLoad())
->diffKeys($examples->safeLoad())
->keys();
if (! $this->missingEnvVariables->isEmpty()) {
$this->markFailed();
}
}
/**
* @return string
*/
protected function formatMissingEnvVariables()
{
return $this->missingEnvVariables->join(', ', ' and ');
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Illuminate\Filesystem\Filesystem;
class EnvFileAnalyzer extends ReliabilityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "A .env file exists for your application.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 10;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application's .env file is missing.";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Filesystem\Filesystem $files
* @return void
*/
public function handle(Filesystem $files)
{
if (! $files->exists(base_path('.env'))) {
$this->markFailed();
}
}
}

View File

@@ -0,0 +1,99 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Dotenv\Dotenv;
class EnvVariableAnalyzer extends ReliabilityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "All env variables defined in your example file are set in your .env file.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 10;
/**
* The missing .env variables.
*
* @var \Illuminate\Support\Collection
*/
protected $missingEnvVariables;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application has some missing environment variables that are defined in your .env.example file "
."but missing in your .env file: ".$this->formatMissingEnvVariables();
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
if (method_exists(Dotenv::class, 'createImmutable')) {
$this->handleDotEnvV4();
return;
}
$examples = Dotenv::create(base_path(), '.env.example');
$actual = Dotenv::create(base_path(), '.env');
$examples->safeLoad();
$actual->safeLoad();
$this->missingEnvVariables = collect($examples->getEnvironmentVariableNames())
->diff($actual->getEnvironmentVariableNames());
if (! $this->missingEnvVariables->isEmpty()) {
$this->markFailed();
}
}
/**
* Execute the analyzer for DotEnv v4.
*/
protected function handleDotEnvV4()
{
$examples = Dotenv::createMutable(base_path(), '.env.example');
$actual = Dotenv::createMutable(base_path(), '.env');
$this->missingEnvVariables = collect($examples->safeLoad())
->diffKeys($actual->safeLoad())
->keys();
if (! $this->missingEnvVariables->isEmpty()) {
$this->markFailed();
}
}
/**
* @return string
*/
protected function formatMissingEnvVariables()
{
return $this->missingEnvVariables->join(', ', ' and ');
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class ForeachIterableAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application only uses iterable types in foreach loops.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to use non-iterable types in foreach loops.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->matchPHPStanAnalysis($PHPStan, ['Argument of an invalid type * supplied for foreach*']);
}
}

View File

@@ -0,0 +1,57 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class InvalidFunctionCallAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not contain invalid function calls.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to contain invalid calls to functions that either do not exist or "
."do not match the function signature.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->matchPHPStanAnalysis($PHPStan, [
'Function * not found*', 'Function * invoked with * parameter* required*',
'Parameter * of function * expects * given*',
]);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class InvalidImportAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not contain invalid imports.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to contain invalid imports.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->matchPHPStanAnalysis($PHPStan, ['Used * not found*']);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class InvalidMethodCallAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not contain invalid method calls.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to contain invalid method calls to methods that either do not exist or "
."do not match the method signature or scope.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->matchPHPStanAnalysis($PHPStan, [
'Method * invoked with *', 'Parameter * of method * is passed by reference, so *',
'Unable to resolve the template *', 'Missing parameter * in call to *',
'Unknown parameter * in call to *', 'Call to method * on an unknown class *',
'Cannot call method * on *', 'Call to private method * of parent class *',
'Call to an undefined method *', 'Call to * method * of class *',
'Call to an undefined static method *', 'Static call to instance method *',
'Calling *::* outside of class scope*', '*::* calls parent::* but *',
'Call to static method * on an unknown class *', 'Cannot call static method * on *',
'Cannot call abstract* method *::*', '* invoked with * parameter* required*',
'Parameter * of * expects * given*', 'Result of * (void) is used*',
'Result of method *',
]);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class InvalidMethodOverrideAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not contain invalid method overrides.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to contain invalid method overrides.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->matchPHPStanAnalysis($PHPStan, [
'Method * overrides final method *', 'Non-static method * overrides static method *',
'Static method * overrides non-static method *', '* method overriding * method * should *',
'Method * overrides method * but misses parameter *', 'Parameter * of method * is passed by reference but *',
'Parameter * of method * is not passed by reference but *', 'Parameter * of method * is not optional*',
'Parameter * of method * is not variadic*', 'Parameter * of method * is not contravariant*',
'Parameter * of method * is variadic*', 'Parameter * of method * is required*',
'Parameter * of method * does not match*', 'Parameter * of method * is not compatible*',
'Return type * of method * is not covariant *', 'Return type * of method * is not compatible *',
]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class InvalidOffsetAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not use invalid offsets.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to to use invalid offsets.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->matchPHPStanAnalysis($PHPStan, [
'Cannot assign* offset * to *', '* does not accept *',
]);
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class InvalidPropertyAccessAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not access class properties in an invalid manner.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to reference class properties that are either undefined "
."or not accessible.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->matchPHPStanAnalysis($PHPStan, [
'Access to * property *', 'Cannot access property * on *',
]);
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class InvalidReturnTypeAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not use invalid return types.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to to use invalid return types. The return type of the method or "
."function does not match the signature.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->matchPHPStanAnalysis($PHPStan, [
'Method * should* return * but * found*', 'Method * with * returns * but should not return *',
'Method * should never return but * found*', 'Method * should* return * but returns *',
'Function * should* return * but * found*', 'Function * with * returns * but should not return *',
'Function * should never return but * found*', 'Function * should* return * but returns *',
]);
}
}

View File

@@ -0,0 +1,52 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Illuminate\Contracts\Foundation\Application;
class MaintenanceModeAnalyzer extends ReliabilityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application is not currently in maintenance mode.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 1;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application is currently in maintenance mode.";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
*/
public function handle(Application $app)
{
if ($app->isDownForMaintenance()) {
$this->markFailed();
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class MissingModelRelationAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not refer to model relations that do not exist.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 10;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to refer to model relations that do not exist. This may be a typo or alternatively, you might need to create these relations in your model class.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->matchPHPStanAnalysis($PHPStan, [
'Relation * is not found in * model*',
]);
}
}

View File

@@ -0,0 +1,53 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class MissingReturnStatementAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not contain missing return statements.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to be missing some return statements.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->parsePHPStanAnalysis($PHPStan, ['return statement is missing']);
}
}

View File

@@ -0,0 +1,118 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesConfigurationFiles;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class QueueTimeoutAnalyzer extends ReliabilityAnalyzer
{
use ParsesConfigurationFiles;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'An appropriate timeout and retry after is set for queues.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 2;
/**
* The name of the queue connection that has an error.
*
* @var string
*/
public $connectionName = null;
/**
* The retry after queue configuration value.
*
* @var int
*/
public $retryAfter = 60;
/**
* The timeout queue configuration value.
*
* @var int
*/
public $timeout = 60;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "The queue timeout value must be at least several seconds shorter than the retry after "
."configuration value. Your {$this->connectionName} queue connection's retry after value "
."is set at {$this->retryAfter} seconds while your timeout value is set at {$this->timeout} "
."seconds. This can cause problems such as your jobs may be processed twice or the queue "
."worker may crash.";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
$connections = collect($config->get('queue.connections', []))
->filter(function ($conf, $queue) {
// skip sqs and sync drivers as they don't have retry after values
return ! in_array($conf['driver'], ['sqs', 'sync']);
})->map(function ($conf, $queue) {
return $this->getTimeoutAndRetryAfter($conf);
})->filter(function ($conf) {
return $conf['timeout'] >= $conf['retry_after'];
});
if ($connections->count() > 0) {
$faultyConnection = $connections->first();
$this->connectionName = $connections->keys()->first();
$this->timeout = $faultyConnection['timeout'];
$this->retryAfter = $faultyConnection['retry_after'];
$this->recordError('queue', 'retry_after', ['connections', $this->connectionName]);
}
}
/**
* Get the timeout and retry after values for the configuration.
*
* @param array $config
* @return array
*/
public function getTimeoutAndRetryAfter(array $config)
{
if (($config['driver'] ?? null) !== 'redis') {
return ['timeout' => 60, 'retry_after' => ($config['retry_after'] ?? 60)];
}
// Timeout is as defined in the horizon config (if app uses Horizon) with fallback to default
// queue worker timeout of 60 seconds.
$timeout = array_reduce(array_merge(
data_get(config('horizon.defaults', []), '*.timeout'),
data_get(config('horizon.environments', []), '*.*.timeout')
), 'max') ?? 60;
return ['timeout' => $timeout, 'retry_after' => ($config['retry_after'] ?? 60)];
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Analyzer;
abstract class ReliabilityAnalyzer extends Analyzer
{
/**
* The category of the analyzer.
*
* @var string|null
*/
public $category = 'Reliability';
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Inspection\Inspector;
class SyntaxErrorAnalyzer extends ReliabilityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'There are no syntax errors in your application code.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application has some PHP files with syntax errors.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\Inspection\Inspector $inspector
* @return void
*/
public function handle(Inspector $inspector)
{
if (count($inspector->errors) > 0) {
$this->markFailed();
foreach ($inspector->errors as $path => $lineNumbers) {
foreach ($lineNumbers as $lineNumber) {
$this->addTrace($path, $lineNumber);
}
}
}
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class UndefinedConstantAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not rely on undefined constants.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to reference undefined constants.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->matchPHPStanAnalysis($PHPStan, [
'* undefined constant *', 'Using * outside of class scope*', 'Access to constant * on an unknown class *',
]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class UndefinedVariableAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not reference undefined variables.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to reference undefined variables.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->matchPHPStanAnalysis($PHPStan, [
'Undefined variable*', 'Variable * might not be defined*',
]);
}
}

View File

@@ -0,0 +1,55 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class UnsetAnalyzer extends ReliabilityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Your application does not try to unset undefined variables.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application seems to unset undefined variables.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $PHPStan
* @return void
*/
public function handle(PHPStan $PHPStan)
{
$this->matchPHPStanAnalysis($PHPStan, [
'Call to function unset* undefined variable*', 'Cannot unset offset *',
]);
}
}

View File

@@ -0,0 +1,60 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Reliability;
use Illuminate\Support\Facades\Artisan;
class UpToDateMigrationsAnalyzer extends ReliabilityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = "Migrations are up-to date.";
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your migrations are not up-to date. Run php artisan migrate to execute the missing migrations.";
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
Artisan::call('migrate', ['--pretend' => 'true', '--force' => 'true']);
if (strstr(Artisan::output(), 'Nothing to migrate.') === false) {
$this->markFailed();
}
}
}

View File

@@ -0,0 +1,58 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesConfigurationFiles;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class AppDebugAnalyzer extends SecurityAnalyzer
{
use ParsesConfigurationFiles;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application hides technical errors in production.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 1;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your app debug is set to true while your application is in production. "
."This can be very dangerous as your app users will be able to view detailed "
."error messages along with stack traces.";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
if ($config->get('app.debug') &&
in_array($config->get('app.env'), ['prod', 'production', 'live'])) {
$this->recordError('app', 'debug');
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesConfigurationFiles;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class AppDebugHideAnalyzer extends SecurityAnalyzer
{
use ParsesConfigurationFiles;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Sensitive environment variables are hidden in non-local environments.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "You haven't set any variables to hide in debug mode while your application seems to be in a non-local "
."environment and set to debug mode. This can be very dangerous as users will be able to view detailed "
."error messages along with sensitive environment variables.";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
if ($config->get('app.debug') &&
$config->get('app.env') !== 'local' &&
empty($config->get('app.debug_blacklist', $config->get('app.debug_hide', [])))) {
$this->recordError('app', 'debug');
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
*/
public function skip()
{
return ! class_exists(\Whoops\Handler\Handler::class);
}
}

View File

@@ -0,0 +1,104 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesConfigurationFiles;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Encryption\Encrypter;
use Illuminate\Support\Str;
class AppKeyAnalyzer extends SecurityAnalyzer
{
use ParsesConfigurationFiles;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Application key is set.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 1;
/**
* The error message describing the analyzer insights.
*
* @var string|null
*/
public $errorMessage = null;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return $this->errorMessage
?? ("Your app key is not set. This can be very dangerous as this key is used "
."to encrypt cookies, signed URLs, model data, job data and session data.");
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
if (! $config->get('app.key')) {
$this->recordError('app', 'key');
return;
}
if (! $config->get('app.cipher')) {
$this->recordError('app', 'cipher');
return;
}
if (! Encrypter::supported($this->parseKey($config->get('app.key')), $config->get('app.cipher'))) {
$this->recordError('app', 'key');
$this->errorMessage = "Your app key and cipher combination is not supported. "
. "This can be very dangerous as this key is used "
. "to encrypt passwords, cookies, signed URLs, model data, CSRF tokens and session data.";
}
}
/**
* Parse the encryption key.
*
* @param string $key
* @return string
*/
protected function parseKey(string $key)
{
if (Str::startsWith($key, $prefix = 'base64:')) {
$key = base64_decode(Str::after($key, $prefix));
}
return $key;
}
}

View File

@@ -0,0 +1,165 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Concerns\AnalyzesMiddleware;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken;
use Illuminate\Routing\Router;
use Illuminate\Support\Str;
class CSRFAnalyzer extends SecurityAnalyzer
{
use AnalyzesMiddleware;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application includes middleware to protect against CSRF attacks.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* The routes that are not protected from CSRF.
*
* @var \Illuminate\Support\Collection
*/
public $unprotectedRoutes;
/**
* Create a new analyzer instance.
*
* @param \Illuminate\Routing\Router $router
* @param \Illuminate\Contracts\Http\Kernel $kernel
* @return void
*/
public function __construct(Router $router, Kernel $kernel)
{
$this->router = $router;
$this->kernel = $kernel;
}
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application is not adequately protected from CSRF attacks. There are {$this->unprotectedRoutes->count()} "
."unprotected routes, which include: {$this->formatUnprotectedRoutes()}. This can be very dangerous and you must "
."resolve this by adding CSRF middleware to your web routes.";
}
/**
* Execute the analyzer.
*
* @return void
* @throws \ReflectionException
*/
public function handle()
{
if ($this->webMiddlewareGroupIsProtected()
|| $this->appIsGloballyProtected()
|| $this->routesAreIndividuallyProtected()) {
return;
}
$this->markFailed();
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
* @throws \ReflectionException
*/
public function skip()
{
// Skip this analyzer if the app is stateless or does not use cookies (app does not need CSRF protection).
return $this->appIsStateless() || ! $this->appUsesCookies();
}
/**
* Determine if the "web" middleware group is protected from CSRF.
*
* @return bool
*/
protected function webMiddlewareGroupIsProtected()
{
if (isset($this->kernel->getMiddlewareGroups()['web'])) {
if (collect($this->kernel->getMiddlewareGroups()['web'])->contains(function ($middleware) {
return is_subclass_of($middleware, VerifyCsrfToken::class);
})) {
// Analysis passed as the web middleware group has the VerifyCsrfToken middleware
return true;
}
}
return false;
}
/**
* Determine if the application is globally protected from CSRF.
*
* @return bool
* @throws \ReflectionException
*/
protected function appIsGloballyProtected()
{
if ($this->appUsesGlobalMiddleware(VerifyCsrfToken::class)) {
// Analysis passed as the VerifyCsrfToken middleware is global
return true;
}
return false;
}
/**
* Determine if all routes are individually protected from CSRF.
*
* @return bool
*/
protected function routesAreIndividuallyProtected()
{
$this->unprotectedRoutes = collect($this->router->getRoutes())->filter(function ($route) {
// Exclude the whitelisted route methods that don't need protection
return collect($route->methods())->contains(function ($method) {
return ! in_array($method, ['HEAD', 'GET', 'OPTIONS']);
});
})->filter(function ($route) {
// Get the routes that don't apply the VerifyCsrfToken middleware
return ! $this->routeUsesMiddleware($route, VerifyCsrfToken::class);
})->filter(function ($route) {
// Exclude the routes that are API routes (do not need CSRF protection)
return ! Str::is('/api/*', $route->uri());
})->map(function ($route) {
// Prettify unprotected routes to display in error message
return '['.implode(',', $route->methods()).'] '.$route->uri();
});
return $this->unprotectedRoutes->count() == 0;
}
/**
* @return string
*/
protected function formatUnprotectedRoutes()
{
return $this->unprotectedRoutes->join(', ', ' and ');
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Concerns\AnalyzesMiddleware;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Cookie\Middleware\EncryptCookies;
use Illuminate\Routing\Router;
class EncryptedCookiesAnalyzer extends SecurityAnalyzer
{
use AnalyzesMiddleware;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application encrypts its cookies.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Create a new analyzer instance.
*
* @param \Illuminate\Routing\Router $router
* @param \Illuminate\Contracts\Http\Kernel $kernel
* @return void
*/
public function __construct(Router $router, Kernel $kernel)
{
$this->router = $router;
$this->kernel = $kernel;
}
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application's cookies are not encrypted. This exposes your application to a wide variety "
."of security risks and potential attacks. An easy fix would be to add the EncryptCookies middleware "
."shipped with Laravel.";
}
/**
* Execute the analyzer.
*
* @return void
* @throws \ReflectionException
*/
public function handle()
{
if ($this->appUsesMiddleware(EncryptCookies::class)) {
return;
}
$this->markFailed();
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
* @throws \ReflectionException
*/
public function skip()
{
// Skip this analyzer if the app is stateless (e.g. API only apps) or doesn't use cookies.
return $this->appIsStateless() || ! $this->appUsesCookies();
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use GuzzleHttp\Client;
use Illuminate\Support\Str;
use Throwable;
class EnvAccessAnalyzer extends SecurityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your .env is not publicly accessible.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* The Guzzle client instance.
*
* @var \GuzzleHttp\Client
*/
protected $client;
/**
* Create a new analyzer instance.
*
* @return void
*/
public function __construct()
{
$this->client = new Client();
}
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your .env file seems to be publicly accessible.";
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
try {
$response = $this->client->get(url('.env'));
if (Str::contains((string) $response->getBody(), ['APP_NAME=', 'APP_ENV=', 'APP_KEY='])) {
$this->markFailed();
}
} catch (Throwable $e) {
return;
}
}
/**
* Set the Guzzle client.
*
* @param \GuzzleHttp\Client $client
*/
public function setClient(Client $client)
{
$this->client = $client;
}
}

View File

@@ -0,0 +1,86 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Illuminate\Support\Str;
class FilePermissionsAnalyzer extends SecurityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your project files and directories use safe permissions.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 60;
/**
* @var string
*/
protected $unsafeFilesOrDirs;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application's project directory permissions are not setup in a secure manner. This may "
."expose your application to be compromised if another account on the same server is vulnerable. "
."This can be even more dangerous if you used shared hosting. All project directories in Laravel "
."should be setup with a max of 775 permissions and most app files should be provided 664 (except "
."executables such as Artisan or your deployment scripts which should be provided 775 permissions). "
."These are the max level of permissions in order to be secure. Your unsafe files or directories "
."include: {$this->unsafeFilesOrDirs}.";
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
$filesOrDirectoriesToCheck = config('enlightn.allowed_permissions', [
base_path() => '775',
app_path() => '775',
resource_path() => '775',
storage_path() => '775',
public_path() => '775',
config_path() => '775',
database_path() => '775',
base_path('routes') => '775',
app()->bootstrapPath() => '775',
app()->bootstrapPath('cache') => '775',
app()->bootstrapPath('app.php') => '664',
base_path('artisan') => '775',
public_path('index.php') => '664',
public_path('server.php') => '664',
]);
$this->unsafeFilesOrDirs = collect($filesOrDirectoriesToCheck)->filter(function ($allowedPermission, $path) {
return file_exists($path) && ($allowedPermission < decoct(fileperms($path) & 0777));
})->keys()->map(function ($path) {
return Str::contains($path, base_path())
? ('['.trim(Str::after($path, base_path()), '/').']') : '['.$path.']';
})->join(', ', ' and ');
if (! empty($this->unsafeFilesOrDirs)) {
$this->markFailed();
}
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class FillableForeignKeyAnalyzer extends SecurityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application does not expose foreign keys for mass assignment.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 10;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application declares potential foreign keys as fillable. This could expose "
."your application to mass assignment attacks.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $phpStan
* @return void
*/
public function handle(PHPStan $phpStan)
{
$this->parsePHPStanAnalysis($phpStan, 'declared as fillable');
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\NPM;
class FrontendVulnerableDependencyAnalyzer extends SecurityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application does not rely on frontend dependencies with known security issues.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 60;
/**
* The NPM instance.
*
* @var \Enlightn\Enlightn\NPM
*/
private $NPM;
/**
* The number of frontend vulnerabilities.
*
* @var int
*/
protected $vulnerabilityCount;
/**
* Create a new analyzer instance.
*
* @param \Enlightn\Enlightn\NPM $NPM
*/
public function __construct(NPM $NPM)
{
$this->NPM = $NPM;
}
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application has a total of {$this->vulnerabilityCount} known vulnerabilities in the application's "
."frontend dependencies. This can be very dangerous and you may investigate this further by running an "
."npm audit or a yarn audit command.";
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
$this->vulnerabilityCount = $this->NPM->countVulnerabilities();
if ($this->vulnerabilityCount > 0) {
$this->markFailed();
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
*/
public function skip()
{
// Skip the analyzer if package.json or npm/yarn does not exist.
return empty($this->NPM->findNpmOrYarn());
}
}

View File

@@ -0,0 +1,94 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Concerns\AnalyzesHeaders;
use Enlightn\Enlightn\Analyzers\Concerns\AnalyzesMiddleware;
use Enlightn\Enlightn\Analyzers\Concerns\DetectsHttps;
use GuzzleHttp\Client;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Routing\Router;
class HSTSHeaderAnalyzer extends SecurityAnalyzer
{
use AnalyzesMiddleware;
use AnalyzesHeaders;
use DetectsHttps;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application includes the HSTS header if it is a HTTPS only app.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Create a new analyzer instance.
*
* @param \Illuminate\Routing\Router $router
* @param \Illuminate\Contracts\Http\Kernel $kernel
* @return void
*/
public function __construct(Router $router, Kernel $kernel)
{
$this->router = $router;
$this->kernel = $kernel;
$this->client = new Client();
}
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application uses HTTPS only cookies, yet it does not include a HSTS (Strict-Transport-Security) "
."response header that tells browsers that it should only be accessed using HTTPS. This may expose your "
."application to man-in-the-middle attacks.";
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
if (! $this->headerExistsOnUrl($this->findLoginRoute(), 'Strict-Transport-Security')) {
$this->markFailed();
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
*/
public function skip()
{
// Skip this analyzer if the app is not an HTTPS only application.
return ! $this->appIsHttpsOnly();
}
}

View File

@@ -0,0 +1,78 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
class HashingStrengthAnalyzer extends SecurityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'A secure hashing strength is configured.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 1;
/**
* The error message describing the analyzer insights.
*
* @var string|null
*/
public $errorMessage = null;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return $this->errorMessage
?? ("Your password hashing strength is set below the recommended threshold. "
."This weakens the app's security against brute-force attacks.");
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
$driver = $config->get('hashing.driver');
if ($driver === 'bcrypt') {
if ($config->get('hashing.bcrypt.rounds') < 12) {
$this->markFailed();
}
} elseif ($driver === 'argon' || $driver === 'argon2id') {
if ($config->get('hashing.argon.memory') < 65536 || $config->get('hashing.argon.time') < 2) {
$this->markFailed();
}
} else {
$this->markSkipped();
}
}
}

View File

@@ -0,0 +1,85 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Concerns\AnalyzesMiddleware;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesConfigurationFiles;
use Illuminate\Contracts\Config\Repository as ConfigRepository;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Routing\Router;
class HttpOnlyCookieAnalyzer extends SecurityAnalyzer
{
use ParsesConfigurationFiles;
use AnalyzesMiddleware;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Cookies are secured as HttpOnly.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 1;
/**
* Create a new analyzer instance.
*
* @param \Illuminate\Routing\Router $router
* @param \Illuminate\Contracts\Http\Kernel $kernel
* @return void
*/
public function __construct(Router $router, Kernel $kernel)
{
$this->router = $router;
$this->kernel = $kernel;
}
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your app session cookies are insecure as the HttpOnly option is disabled in your "
."session configuration. This exposes your application to possible XSS (cross-site "
."scripting) attacks.";
}
/**
* Execute the analyzer.
*
* @param \Illuminate\Contracts\Config\Repository $config
* @return void
*/
public function handle(ConfigRepository $config)
{
if (! $config->get('session.http_only', false)) {
$this->recordError('session', 'http_only');
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
* @throws \ReflectionException
*/
public function skip()
{
return $this->appIsStateless();
}
}

View File

@@ -0,0 +1,97 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Composer;
class LicenseAnalyzer extends SecurityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application does not rely on dependencies you are not legally allowed to use.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 60;
/**
* The blacklisted packages.
*
* @var \Illuminate\Support\Collection
*/
public $blacklistedPackages;
/**
* All the packages used by the application.
*
* @var \Illuminate\Support\Collection
*/
public $allPackages;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application has a total of {$this->blacklistedPackages->count()} package(s) that you may not be legally "
."allowed to use. By default, we assume the MIT, Apache-2.0, ISC, BSD Clause 2 & 3 and LGPL licenses to be "
."legally valid for use for proprietary or commercial applications. However, you are free to change this "
."in the Enlightn config. Unsafe packages include {$this->formatBlacklistedPackages()}";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\Composer $composer
* @return void
*/
public function handle(Composer $composer)
{
$whitelistedLicenses = array_map('strtoupper', config('enlightn.license_whitelist', [
'Apache-2.0', 'Apache2', 'BSD-2-Clause', 'BSD-3-Clause', 'LGPL-2.1-only', 'LGPL-2.1',
'LGPL-2.1-or-later', 'LGPL-3.0', 'LGPL-3.0-only', 'LGPL-3.0-or-later', 'MIT', 'ISC',
'CC0-1.0', 'Unlicense', 'WTFPL',
]));
$commercialPackages = config('enlightn.commercial_packages', []);
$this->allPackages = $composer->getLicenses();
$this->blacklistedPackages = $this->allPackages->map(function ($licenses) {
return array_map('strtoupper', $licenses);
})->filter(function ($licenses, $package) use ($whitelistedLicenses, $commercialPackages) {
// Get all packages that have any licenses that are not whitelisted
return empty(array_intersect($licenses, $whitelistedLicenses))
&& ! in_array($package, $commercialPackages);
});
if ($this->blacklistedPackages->count() > 0) {
$this->markFailed();
}
}
/**
* @return string
*/
public function formatBlacklistedPackages()
{
return $this->allPackages->intersectByKeys($this->blacklistedPackages)
->map(function ($licenses, $package) {
return '['.$package.': '.implode(', ', $licenses).']';
})->join(', ', ' and ');
}
}

View File

@@ -0,0 +1,179 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Concerns\AnalyzesMiddleware;
use Enlightn\Enlightn\Analyzers\Concerns\InspectsCode;
use Enlightn\Enlightn\Inspection\Inspector;
use Enlightn\Enlightn\Inspection\QueryBuilder;
use Illuminate\Cache\RateLimiter;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Routing\Middleware\ThrottleRequests;
use Illuminate\Routing\Router;
use Illuminate\Session\Middleware\StartSession;
use Illuminate\Support\Facades\RateLimiter as RateLimiterFacade;
use Illuminate\Support\Str;
class LoginThrottlingAnalyzer extends SecurityAnalyzer
{
use AnalyzesMiddleware;
use InspectsCode;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application includes login throttling for protection against brute force attacks.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Create a new analyzer instance.
*
* @param \Illuminate\Routing\Router $router
* @param \Illuminate\Contracts\Http\Kernel $kernel
* @return void
*/
public function __construct(Router $router, Kernel $kernel)
{
$this->router = $router;
$this->kernel = $kernel;
}
/**
* The routes that are not protected from CSRF.
*
* @var \Illuminate\Support\Collection
*/
public $unprotectedRoutes;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application is not adequately protected from brute force attacks. This can be very dangerous and "
."you may resolve this by adding appropriate login throttling middleware to your login routes. We make an "
."educated guess that the following routes could be unprotected login routes: {$this->formatUnprotectedRoutes()}. "
."You may ignore this in case the throttling is already setup at the web server (Nginx, Apache) level instead "
."of the Laravel application level or in case you have devised your own custom throttling mechanism and are not "
."using the throttling middleware or RateLimiter class that ships with the Laravel Framework.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\Inspection\Inspector $inspector
* @return void
* @throws \ReflectionException
*/
public function handle(Inspector $inspector)
{
if ($this->appUsesMiddleware(ThrottleRequests::class)
|| $this->appUsesRateLimiterFacade($inspector)
|| $this->appUsesRateLimiterInstance($inspector)) {
// We simply assume that if either a throttling middleware or the application uses the RateLimiter class,
// then login is throttled properly. This is smarter than guessing the name of the login route.
return;
}
if ($this->canFindLoginRoute()) {
// If we can't guess the login route, we're not marking it as failed. There could be use cases where
// some apps don't have login routes at all but are still stateful (e.g. the login could be proxied
// from another app in the same domain that share cookies like a dashboard app).
$this->markFailed();
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
* @throws \ReflectionException
*/
public function skip()
{
// Skip this analyzer if the app is stateless (there is no login for stateless apps) or if the app
// uses the laravel/ui package (that handles login throttling without middleware).
return $this->appIsStateless() || trait_exists(\Illuminate\Foundation\Auth\AuthenticatesUsers::class)
// Fortify also uses it's own authentication pipeline that has throttling. We skip this analyzer if we find
// that the app uses Fortify's in-built login throttling.
|| (class_exists(\Laravel\Fortify\LoginRateLimiter::class) && is_null(config('fortify.limiters.login')));
}
/**
* Determine whether we can find the login route.
*
* @return bool
*/
protected function canFindLoginRoute()
{
$this->unprotectedRoutes = collect($this->router->getRoutes())->filter(function ($route) {
// Exclude all non-POST route methods that don't need protection
return collect($route->methods())->contains(function ($method) {
return $method === 'POST';
});
})->filter(function ($route) {
// Get all the routes that are "stateful" (exclude stateless API routes that don't need login)
return $this->appUsesGlobalMiddleware(StartSession::class)
|| $this->routeUsesMiddleware($route, StartSession::class);
})->filter(function ($route) {
// Here we're just filtering out all the routes by guessing the login route name
return Str::contains(strtolower($route->uri()), ['login', 'signin', 'auth']);
})->map(function ($route) {
// Prettify unprotected routes to display in error message
return $route->uri();
});
return $this->unprotectedRoutes->count() > 0;
}
/**
* Determine whether the app uses the RateLimiter facade
*
* @param \Enlightn\Enlightn\Inspection\Inspector $inspector
* @return bool
*/
protected function appUsesRateLimiterFacade(Inspector $inspector)
{
$builder = (new QueryBuilder())->hasStaticCall(RateLimiterFacade::class, 'hit');
return $this->passesCodeInspection($inspector, $builder);
}
/**
* Determine whether the app uses the RateLimiter facade
*
* @param \Enlightn\Enlightn\Inspection\Inspector $inspector
* @return bool
*/
protected function appUsesRateLimiterInstance(Inspector $inspector)
{
$builder = (new QueryBuilder())->instantiates(RateLimiter::class);
return $this->passesCodeInspection($inspector, $builder);
}
/**
* @return string
*/
protected function formatUnprotectedRoutes()
{
return $this->unprotectedRoutes->join(', ', ' and ');
}
}

View File

@@ -0,0 +1,56 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Concerns\ParsesPHPStanAnalysis;
use Enlightn\Enlightn\PHPStan;
class MassAssignmentAnalyzer extends SecurityAnalyzer
{
use ParsesPHPStanAnalysis;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application is not exposed to mass assignment vulnerabilities.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 10;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application passes user controlled request data directly into the database. This "
."exposes your application to mass assignment SQL injection vulnerabilities. Use the Request "
."object's only or validated methods to restrict the database columns to the ones that are "
."intended to be modified to fix these vulnerabilities.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\PHPStan $phpStan
* @return void
*/
public function handle(PHPStan $phpStan)
{
$this->parsePHPStanAnalysis($phpStan, 'may result in a mass assignment vulnerability');
}
}

View File

@@ -0,0 +1,100 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
class PHPIniAnalyzer extends SecurityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your PHP configuration is secure.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* A collection of insecure PHP ini settings.
*
* @var \Illuminate\Support\Collection
*/
protected $insecureSettings;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application does not set secure php.ini configuration values. This may expose your application "
."to security vulnerabilities. Unless there is a very specific use case for your application, it is "
."recommended to set your php.ini configuration as follows: {$this->formatRecommendations()}.";
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
$secureSettings = config('enlightn.php_secure_settings', [
'allow_url_fopen' => false,
'allow_url_include' => false,
'expose_php' => false,
'display_errors' => false,
'display_startup_errors' => false,
'log_errors' => true,
'ignore_repeated_errors' => false,
]);
$this->insecureSettings = collect($secureSettings)->filter(function ($expected, $var) {
return ! in_array(strtolower(ini_get($var)), $expected ? ['1', 'on', 'yes', 'true'] : ['0', 'off', '', 'no', 'false']);
});
if ($this->insecureSettings->count() > 0) {
$this->markFailed();
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
*/
public function skip()
{
// Skip this analyzer if the app environment is local.
return $this->isLocalAndShouldSkip();
}
/**
* @return string
*/
protected function formatRecommendations()
{
return $this->insecureSettings->map(function ($result, $var) {
return "[{$var}: ".($result ? 'On or 1' : 'Off or 0')."]";
})->join(', ', ' and ');
}
}

View File

@@ -0,0 +1,15 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Analyzer;
abstract class SecurityAnalyzer extends Analyzer
{
/**
* The category of the analyzer.
*
* @var string|null
*/
public $category = 'Security';
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Composer;
use Illuminate\Support\Str;
class StableDependencyAnalyzer extends SecurityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application uses stable versions of dependencies.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MINOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 1;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application's dependencies are unstable versions. These may include bug fixes and/or security "
."patches. It is recommended to update to the most stable versions.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\Composer $composer
* @return void
*/
public function handle(Composer $composer)
{
if (Str::contains($composer->updateDryRun(['--prefer-stable']), ['Upgrading', 'Downgrading'])) {
$this->markFailed();
}
}
}

View File

@@ -0,0 +1,63 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Concerns\InspectsCode;
use Enlightn\Enlightn\Inspection\Inspector;
use Enlightn\Enlightn\Inspection\QueryBuilder;
use Illuminate\Database\Eloquent\Model;
class UnguardedModelsAnalyzer extends SecurityAnalyzer
{
use InspectsCode;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application does not un-guard models.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 30;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application un-guards models, which guards against mass assignment vulnerabilities. "
."The Laravel Framework includes this protection by default and it is advisable not to override "
."this check. While properly validating requests can mitigate the risk, guarding models by "
."default makes your code more readable towards mass assignment vulnerabilities. For instance, "
."an alternative to un-guarding models, is using the forceFill method on the model. While typing "
."or reviewing this code, it will be much more obvious to developers to validate the request "
."before force-filling the model.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\Inspection\Inspector $inspector
* @return void
*/
public function handle(Inspector $inspector)
{
$builder = (new QueryBuilder())->doesntHaveStaticCall(Model::class, 'unguard');
$this->inspectCode($inspector, $builder);
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Composer;
use Illuminate\Support\Str;
class UpToDateDependencyAnalyzer extends SecurityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Dependencies are up-to-date.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MINOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 1;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application's dependencies are not up-to-date. These may include bug fixes and/or security "
."patches.";
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\Composer $composer
* @return void
*/
public function handle(Composer $composer)
{
// First string match is for Composer 1 and the second one is for Composer 2.
// First check is for all dependencies and second check is for production dependencies.
if (! Str::contains(
$composer->installDryRun(),
['Nothing to install or update', 'Nothing to install, update or remove']
) && ! Str::contains(
$composer->installDryRun(['--no-dev']),
['Nothing to install or update', 'Nothing to install, update or remove']
)) {
$this->markFailed();
}
}
}

View File

@@ -0,0 +1,93 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Composer;
use Enlightn\SecurityChecker\AdvisoryAnalyzer;
use Enlightn\SecurityChecker\AdvisoryFetcher;
use Enlightn\SecurityChecker\AdvisoryParser;
use Enlightn\SecurityChecker\Composer as SecurityCheckerComposer;
use Throwable;
class VulnerableDependencyAnalyzer extends SecurityAnalyzer
{
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application does not rely on backend dependencies with known security issues.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_CRITICAL;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 60;
/**
* The result of the vulnerability scan.
*
* @var array
*/
public $result;
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return "Your application has a total of ".count($this->result)." known vulnerabilities in the application "
."dependencies. This can be very dangerous and you must resolve this by either applying patch updates or "
."removing the vulnerable dependencies. The packages which have these vulnerabilities include: "
.PHP_EOL.$this->listVulnerablePackages();
}
/**
* Execute the analyzer.
*
* @param \Enlightn\Enlightn\Composer $composer
* @return void
* @throws \GuzzleHttp\Exception\GuzzleException
*/
public function handle(Composer $composer)
{
$parser = new AdvisoryParser((new AdvisoryFetcher)->fetchAdvisories());
$dependencies = (new SecurityCheckerComposer)->getDependencies($composer->getLockFile());
$this->result = (new AdvisoryAnalyzer($parser->getAdvisories()))->analyzeDependencies($dependencies);
if (count($this->result) > 0) {
$this->markFailed();
}
}
/**
* List the vulnerable packages.
*
* @return string
*/
public function listVulnerablePackages()
{
try {
return collect($this->result)
->map(function ($vulnerability, $package) {
return $package.' ('.$vulnerability['version'].'): '.
collect(data_get($vulnerability, 'advisories.*.title'))
->join(', ', ' and ');
})->values()->implode(PHP_EOL);
} catch (Throwable $e) {
return json_encode($this->result);
}
}
}

View File

@@ -0,0 +1,105 @@
<?php
namespace Enlightn\Enlightn\Analyzers\Security;
use Enlightn\Enlightn\Analyzers\Concerns\AnalyzesHeaders;
use Enlightn\Enlightn\Analyzers\Concerns\AnalyzesMiddleware;
use GuzzleHttp\Client;
use Illuminate\Contracts\Http\Kernel;
use Illuminate\Routing\Router;
use Illuminate\Support\Str;
class XSSAnalyzer extends SecurityAnalyzer
{
use AnalyzesMiddleware;
use AnalyzesHeaders;
/**
* The title describing the analyzer.
*
* @var string|null
*/
public $title = 'Your application sets appropriate HTTP headers to protect against XSS attacks.';
/**
* The severity of the analyzer.
*
* @var string|null
*/
public $severity = self::SEVERITY_MAJOR;
/**
* The time to fix in minutes.
*
* @var int|null
*/
public $timeToFix = 5;
/**
* Determine whether the analyzer should be run in CI mode.
*
* @var bool
*/
public static $runInCI = false;
/**
* Create a new analyzer instance.
*
* @param \Illuminate\Routing\Router $router
* @param \Illuminate\Contracts\Http\Kernel $kernel
* @return void
*/
public function __construct(Router $router, Kernel $kernel)
{
$this->router = $router;
$this->kernel = $kernel;
$this->client = new Client();
}
/**
* Get the error message describing the analyzer insights.
*
* @return string
*/
public function errorMessage()
{
return 'Your application is not adequately protected from XSS attacks. The Content-Security-Policy '
.'is either not set or not set adequately for XSS. It is recommended to set a "script-src" or '
.'"default-src" policy directive without "unsafe-eval" or "unsafe-inline".';
}
/**
* Execute the analyzer.
*
* @return void
*/
public function handle()
{
$headers = $this->getHeadersOnUrl($url = $this->findLoginRoute(), 'Content-Security-Policy');
if (! isset($headers)) {
$policy = get_meta_tags($url)['Content-Security-Policy'] ?? '';
if (empty($policy)) {
$this->markFailed();
} elseif (! Str::contains($policy, ['default-src', 'script-src']) || Str::contains($policy, 'unsafe')) {
$this->markFailed();
}
} elseif (! collect($headers)->contains(function ($header) {
return Str::contains($header, ['default-src', 'script-src']) && ! Str::contains($header, 'unsafe');
})) {
$this->markFailed();
}
}
/**
* Determine whether to skip the analyzer.
*
* @return bool
* @throws \ReflectionException
*/
public function skip()
{
// Skip this analyzer if the app is stateless (e.g. API only apps).
return $this->isLocalAndShouldSkip() || $this->appIsStateless();
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Enlightn\Enlightn\Analyzers;
use Illuminate\Support\Str;
use JsonSerializable;
use RuntimeException;
class Trace implements JsonSerializable
{
/**
* @var int
*/
public $lineNumber;
/**
* @var string|null
*/
public $details;
/**
* @var string|null
*/
public $path;
/**
* @var array
*/
public $codeSnippet = [];
/**
* @var int
*/
private $snippetLineCount = 30;
public function __construct($path, $lineNumber, $details = null)
{
$this->path = $path;
$this->lineNumber = $lineNumber;
$this->details = $details;
}
/**
* @return array
*/
public function codeSnippet()
{
if (! file_exists($path = $this->absolutePath())) {
return [];
}
if (! empty($this->codeSnippet)) {
// Return cached value if already computed.
return $this->codeSnippet;
}
try {
$file = new File($path);
[$startLineNumber, $endLineNumber] = $this->getBounds($file->numberOfLines());
$codeSnippet = [];
$line = $file->getLine($startLineNumber);
$currentLineNumber = $startLineNumber;
while ($currentLineNumber <= $endLineNumber) {
$codeSnippet[$currentLineNumber] = rtrim(substr($line, 0, 250));
$line = $file->getNextLine();
$currentLineNumber++;
}
$this->codeSnippet = $codeSnippet;
return $this->codeSnippet;
} catch (RuntimeException $exception) {
return [];
}
}
/**
* @return string|null
*/
public function relativePath()
{
return trim(Str::contains($this->path, base_path()) ? Str::after($this->path, base_path()) : $this->path, '/');
}
/**
* @return string|null
*/
public function absolutePath()
{
if (! file_exists($this->path)) {
return base_path(trim($this->path, '/'));
}
return $this->path;
}
/**
* @param int $snippetLineCount
* @return $this
*/
public function setSnippetLineCount(int $snippetLineCount)
{
$this->snippetLineCount = $snippetLineCount;
return $this;
}
/**
* @return array
*/
public function jsonSerialize()
{
return [
'path' => $this->relativePath(),
'lineNumber' => $this->lineNumber,
'details' => $this->details,
'codeSnippet' => $this->codeSnippet(),
];
}
/**
* @param int $totalLines
* @return array
*/
private function getBounds(int $totalLines)
{
$startLine = max($this->lineNumber - floor($this->snippetLineCount / 2), 1);
$endLine = $startLine + ($this->snippetLineCount - 1);
if ($endLine > $totalLines) {
$endLine = $totalLines;
$startLine = max($endLine - ($this->snippetLineCount - 1), 1);
}
return [$startLine, $endLine];
}
}

View File

@@ -0,0 +1,92 @@
<?php
namespace Enlightn\Enlightn\CodeCorrection;
use Illuminate\Filesystem\Filesystem;
use PhpParser\Lexer\Emulative;
use PhpParser\Node\Expr\Array_;
use PhpParser\Node\Expr\ArrayItem;
use PhpParser\Node\Scalar\String_;
use PhpParser\Node\Stmt\Return_;
use PhpParser\NodeTraverser;
use PhpParser\NodeVisitor\CloningVisitor;
use PhpParser\Parser\Php7;
use PhpParser\PrettyPrinter\Standard;
class ConfigManipulator
{
use ConstructsConfigurationAST;
/**
* Get the config values as pretty printed code.
*
* @param array $configValues
* @return string
*/
public function get($configValues = [])
{
$items = [];
foreach ($configValues as $key => $configValue) {
$items[] = new ArrayItem(
$this->getConfiguration($configValue),
new String_($key)
);
}
$ast = [new Array_($items)];
return (new Standard(['shortArraySyntax' => true]))->prettyPrint($ast);
}
/**
* Modify the config file with the given config values.
*
* @param string $configFilePath
* @param array $configValues
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function replace(string $configFilePath, $configValues = [])
{
$lexer = new Emulative([
'usedAttributes' => [
'comments',
'startLine', 'endLine',
'startTokenPos', 'endTokenPos',
],
]);
$parser = new Php7($lexer);
$ast = $parser->parse((new Filesystem)->get($configFilePath));
$oldTokens = $lexer->getTokens();
$traverser = new NodeTraverser;
$traverser->addVisitor(new CloningVisitor);
$config = require $configFilePath;
foreach ($configValues as $key => $configValue) {
if (isset($config[$key])) {
$traverser->addVisitor(new ConfigReplacementNodeVisitor($key, $configValue));
}
}
$newAst = $traverser->traverse($ast);
foreach ($configValues as $key => $configValue) {
if (! isset($config[$key])
&& isset($newAst[0])
&& $newAst[0] instanceof Return_
&& $newAst[0]->expr instanceof Array_) {
$newAst[0]->expr->items[] = new ArrayItem(
$this->getConfiguration($configValue),
new String_($key)
);
}
}
$newCode = (new Standard(['shortArraySyntax' => true]))->printFormatPreserving($newAst, $ast, $oldTokens);
(new Filesystem)->put($configFilePath, $newCode);
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Enlightn\Enlightn\CodeCorrection;
use PhpParser\Node;
use PhpParser\NodeVisitorAbstract;
class ConfigReplacementNodeVisitor extends NodeVisitorAbstract
{
use ConstructsConfigurationAST;
/**
* @var string
*/
protected $configKey;
/**
* @var string|array
*/
protected $configValue;
public function __construct($configKey = null, $configValue = null)
{
$this->configKey = $configKey;
$this->configValue = $configValue;
}
public function leaveNode(Node $node)
{
if ($node instanceof Node\Expr\ArrayItem
&& $node->key instanceof Node\Scalar\String_
&& $node->key->value === $this->configKey) {
$node->value = $this->getConfiguration($this->configValue);
}
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Enlightn\Enlightn\CodeCorrection;
use Illuminate\Support\Str;
use PhpParser\Node;
trait ConstructsConfigurationAST
{
/**
* Only supports arrays or strings.
*
* @param $value
* @return \PhpParser\Node\Expr\Array_|\PhpParser\Node\Expr\ClassConstFetch|\PhpParser\Node\Scalar\String_
*/
public function getConfiguration($value)
{
if (is_string($value)) {
if (Str::contains($value, '\\') && class_exists($value)) {
return new Node\Expr\ClassConstFetch(new Node\Name($value), new Node\Identifier('class'));
}
return new Node\Scalar\String_($value);
} elseif (is_array($value)) {
$items = [];
foreach ($value as $arrayKey => $arrayValue) {
if (is_string($arrayKey)) {
// Assuming associative array.
$items[] = new Node\Expr\ArrayItem($this->getConfiguration($arrayValue), $this->getConfiguration($arrayKey));
} else {
// Assuming sequential array.
$items[] = new Node\Expr\ArrayItem($this->getConfiguration($arrayValue), null);
}
}
return new Node\Expr\Array_($items);
}
}
}

View File

@@ -0,0 +1,14 @@
<?php
namespace Enlightn\Enlightn;
class CommitHash
{
/**
* @return string
*/
public static function get()
{
return trim(exec('cd '.app_path().' && git log --pretty="%h" -n1 HEAD 2> /dev/null'));
}
}

View File

@@ -0,0 +1,132 @@
<?php
namespace Enlightn\Enlightn;
use Illuminate\Support\Composer as BaseComposer;
class Composer extends BaseComposer
{
/**
* Get the package dependencies.
*
* @param bool $includeDev
* @return array
*/
public function getDependencies($includeDev = true)
{
$arguments = ['show', '-N'];
if (! $includeDev) {
$arguments[] = '--no-dev';
}
return array_map(
'trim',
array_filter(explode("\n", $this->runCommand($arguments, false)))
);
}
/**
* Get the licenses of all packages used by the application.
*
* @param bool $excludeDev
* @return \Illuminate\Support\Collection
*/
public function getLicenses($excludeDev = true)
{
$arguments = ['licenses', '--format=json'];
if ($excludeDev) {
$arguments[] = '--no-dev';
}
return collect(json_decode($this->runCommand($arguments, false), true)['dependencies'] ?? [])
->map(function ($value) {
return $value['license'];
});
}
/**
* Run a dry run Composer install.
*
* @param array $options
* @return string
*/
public function installDryRun(array $options = [])
{
return $this->runCommand(array_merge(['install', '--dry-run'], $options));
}
/**
* Run a dry run Composer update.
*
* @param array $options
* @return string
*/
public function updateDryRun(array $options = [])
{
return $this->runCommand(array_merge(['update', '--dry-run'], $options));
}
/**
* Run any Composer command and get the output.
*
* @param array $options
* @param bool $includeErrorOutput
* @return string
*/
public function runCommand(array $options = [], $includeErrorOutput = true)
{
$composer = $this->findComposer();
$command = array_merge(
(array) $composer,
$options
);
$process = $this->getProcess($command);
$process->run();
return $process->getOutput().($includeErrorOutput ? $process->getErrorOutput() : '');
}
/**
* Get the composer lock file location.
*
* @return string|null
*/
public function getLockFile()
{
if ($this->files->exists($this->workingPath.'/composer.lock')) {
return $this->workingPath.'/composer.lock';
}
return null;
}
/**
* Get the composer lock file location.
*
* @return array
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
public function getJson()
{
return json_decode($this->files->get($this->getJsonFile()), true);
}
/**
* Get the composer lock file location.
*
* @return string|null
*/
public function getJsonFile()
{
if ($this->files->exists($this->workingPath.'/composer.json')) {
return $this->workingPath.'/composer.json';
}
return null;
}
}

View File

@@ -0,0 +1,160 @@
<?php
namespace Enlightn\Enlightn\Console;
use Enlightn\Enlightn\Analyzers\Trace;
use Enlightn\Enlightn\CodeCorrection\ConfigManipulator;
use Enlightn\Enlightn\Enlightn;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
class BaselineCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'enlightn:baseline
{--ci : Run Enlightn in CI Mode}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Declare the currently reported errors as the baseline to avoid reporting them in subsequent runs.';
/**
* @var array
*/
protected $dont_report = [];
/**
* @var array
*/
protected $ignore_errors = [];
/**
* @var \Symfony\Component\Console\Helper\ProgressBar
*/
protected $progressbar;
/**
* Execute the console command.
*
* @return int
* @throws \ReflectionException|\Illuminate\Contracts\Container\BindingResolutionException|\Throwable
*/
public function handle()
{
$this->setColors();
$this->line(require __DIR__.DIRECTORY_SEPARATOR.'logo.php');
$this->output->newLine();
$this->line('Please wait while Enlightn scans your code base...');
$this->output->newLine();
// Reset ignored errors to establish a complete baseline.
config()->set('enlightn.ignore_errors', []);
if ($this->option('ci')) {
Enlightn::filterAnalyzersForCI();
}
Enlightn::register();
$this->progressbar = $this->output->createProgressBar(count(Enlightn::$analyzerClasses));
Enlightn::using([$this, 'parseAnalyzerResult']);
Enlightn::run($this->laravel);
$this->progressbar->finish();
$this->output->newLine();
$this->updateConfig();
return 0;
}
/**
* Parse the result of each analyzer.
*
* @param array $info
* @return void
*/
public function parseAnalyzerResult(array $info)
{
if ($info['status'] === 'failed' && empty($info['traces'])) {
$this->dont_report[] = $info['class'];
}
if (empty($info['traces'])) {
return;
}
collect($info['traces'])->each(function (Trace $trace) use ($info) {
if (is_null($trace->details)) {
if (! in_array($info['class'], $this->dont_report)) {
$this->dont_report[] = $info['class'];
}
} else {
if (! isset($this->ignore_errors[$info['class']])) {
$this->ignore_errors[$info['class']] = [];
}
$this->ignore_errors[$info['class']][] = [
'path' => Str::contains($trace->path, base_path()) ?
trim(Str::after($trace->path, base_path()), '/') : $trace->path,
'details' => $trace->details,
];
}
});
$this->progressbar->advance();
}
/**
* Update the config file for new baseline.
*
* @throws \Illuminate\Contracts\Filesystem\FileNotFoundException
*/
protected function updateConfig()
{
if (! file_exists(config_path('enlightn.php'))) {
if (Enlightn::isPro()) {
$this->call('vendor:publish', ['--tag' => 'enlightnpro']);
} else {
$this->call('vendor:publish', ['--tag' => 'enlightn']);
}
}
(new ConfigManipulator)->replace(config_path('enlightn.php'), [
'dont_report' => $this->dont_report,
'ignore_errors' => $this->ignore_errors,
]);
$this->info("Successfully updated config/enlightn.php with new baseline values.");
}
/**
* Set the console colors for Enlightn's logo.
*
* @return void
*/
protected function setColors()
{
collect([
'e' => 'green',
'n' => 'green',
'l' => 'green',
'i' => 'green',
'g' => 'green',
'h' => 'green',
't' => 'green',
'ns' => 'green',
])->each(function ($color, $tag) {
$this->output->getFormatter()->setStyle($tag, new OutputFormatterStyle($color));
});
}
}

View File

@@ -0,0 +1,199 @@
<?php
namespace Enlightn\Enlightn\Console;
use Enlightn\Enlightn\Console\Formatters\AnsiFormatter;
use Enlightn\Enlightn\Enlightn;
use Enlightn\Enlightn\Reporting\API;
use Enlightn\Enlightn\Reporting\JsonReportBuilder;
use Illuminate\Console\Command;
class EnlightnCommand extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'enlightn
{analyzer?* : The analyzer class that you wish to run}
{--details : Show details of each failed check}
{--ci : Run Enlightn in CI Mode}
{--report : Compile a report to trigger a comment by the Enlightn Github Bot}
{--review : Enable this for a review of the diff by the Enlightn Github Bot}
{--show-exceptions : Display the stack trace of exceptions if any}
{--issue= : The issue number of the pull request for the Enlightn Github Bot}
{--hash= : An optional alternative commit hash to report to the Web UI}';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Enlightn your application!';
/**
* The final result of the analysis.
*
* @var array
*/
public $result = [];
/**
* The number of analyzers to run.
*
* @var int
*/
protected $totalAnalyzers;
/**
* The number of analyzers that have completed their analysis.
*
* @var int
*/
protected $countAnalyzers;
/**
* The analyzer classes to run. All classes will run if empty.
*
* @var array
*/
protected $analyzerClasses;
/**
* @var \Enlightn\Enlightn\Console\Formatters\Formatter
*/
protected $formatter;
/**
* @var array
*/
protected $analyzerInfos = [];
/**
* Execute the console command.
*
* @param \Enlightn\Enlightn\Reporting\API $api
* @return int
* @throws \Illuminate\Contracts\Container\BindingResolutionException
* @throws \ReflectionException
* @throws \Throwable
*/
public function handle(API $api)
{
$this->analyzerClasses = $this->argument('analyzer');
$this->formatter = new AnsiFormatter;
$this->formatter->beforeAnalysis($this);
if ($this->option('ci')) {
Enlightn::filterAnalyzersForCI();
}
Enlightn::register($this->analyzerClasses);
$this->totalAnalyzers = Enlightn::totalAnalyzers();
$this->countAnalyzers = 1;
$this->initializeResult();
Enlightn::using([$this, 'printAnalyzerOutput']);
Enlightn::run($this->laravel);
$this->formatter->afterAnalysis($this, empty($this->analyzerClasses));
if ($this->option('report')) {
$reportBuilder = new JsonReportBuilder();
$metadata = [];
if ($github_issue = $this->option('issue')) {
$metadata = compact('github_issue');
}
if ($this->option('review')) {
$metadata['needs_review'] = true;
}
if ($this->option('ci')) {
$metadata['trigger'] = 'ci';
}
if ($hash = $this->option('hash')) {
$metadata['commit_id'] = $hash;
}
$url = $api->sendReport($reportBuilder->buildReport($this->analyzerInfos, $this->result, $metadata));
if (! is_null($url)) {
$this->getOutput()->newLine();
$this->comment("Your report can be viewed at <href={$url}>{$url}</>");
}
}
// Exit with a non-zero exit code if there were failed checks to throw an error on CI environments
return collect($this->result)->sum(function ($category) {
return $category['reported'];
}) == 0 ? 0 : 1;
}
/**
* @param array $info
*
* @return void
*/
public function printAnalyzerOutput(array $info)
{
$this->analyzerInfos[] = $info;
$this->formatter->parseAnalyzerResult(
$this,
$info,
$this->countAnalyzers,
$this->totalAnalyzers,
empty($this->analyzerClasses)
);
$this->updateResult($info);
$this->countAnalyzers++;
}
/**
* Initialize the result.
*
* @return $this
*/
protected function initializeResult()
{
$this->result = [];
foreach (array_merge(Enlightn::$categories, ['Total']) as $category) {
$this->result[$category] = [
'passed' => 0,
'failed' => 0,
'skipped' => 0,
'error' => 0,
'reported' => 0,
];
}
return $this;
}
/**
* Update the result based on the analysis.
*
* @param array $info
* @return string
*/
protected function updateResult(array $info)
{
$this->result[$info['category']][$info['status']]++;
$this->result['Total'][$info['status']]++;
if ($info['status'] === 'failed' && ($info['reportable'] ?? true)) {
$this->result[$info['category']]['reported']++;
$this->result['Total']['reported']++;
}
}
}

View File

@@ -0,0 +1,243 @@
<?php
namespace Enlightn\Enlightn\Console\Formatters;
use Enlightn\Enlightn\Analyzers\Trace;
use Enlightn\Enlightn\Enlightn;
use Illuminate\Console\Command;
use Illuminate\Console\OutputStyle;
use Symfony\Component\Console\Formatter\OutputFormatterStyle;
use Symfony\Component\Console\Helper\TableStyle;
class AnsiFormatter implements Formatter
{
/**
* The category of the analyzers currently being run.
*
* @var string|null
*/
protected $category = null;
/**
* Indicates whether to limit the number of lines or files displayed in each check.
*
* @var bool
*/
protected $compactLines;
/**
* Called before analysis for initialization or printing a greeting message.
*
* @param \Illuminate\Console\Command $command
* @return void
*/
public function beforeAnalysis(Command $command)
{
$this->setColors($command->getOutput());
$command->line(require __DIR__.DIRECTORY_SEPARATOR.'..'.DIRECTORY_SEPARATOR.'logo.php');
$command->getOutput()->newLine();
$command->line('Please wait while Enlightn scans your code base...');
$this->compactLines = config('enlightn.compact_lines', true);
}
/**
* Called for each analyzer with the result.
*
* @param \Illuminate\Console\Command $command
* @param array $result
* @param int $current
* @param int $total
* @param bool $allAnalyzers
* @return void
*/
public function parseAnalyzerResult(Command $command, array $result, int $current, int $total, bool $allAnalyzers)
{
if ($this->category !== $result['category']) {
$command->getOutput()->newLine();
$command->line('|------------------------------------------');
$command->line('| Running '.$result['category'].' Checks');
$command->line('|------------------------------------------');
}
$command->getOutput()->newLine();
$command->getOutput()->write("<fg=yellow>Check {$current}/{$total}: </fg=yellow>");
$command->getOutput()->write($result['title']);
$command->line(' '.$this->getSymbolForStatus($result['status']));
if (! in_array($result['status'], ['passed', 'skipped'])) {
$error = $result['error'] ?? $result['exception'];
$command->line("<fg=red>{$error}</fg=red>");
if ($result['status'] === 'error' && $command->option('show-exceptions')) {
$command->line("<fg=red>{$result['stackTrace']}</fg=red>");
}
if (! empty($result['traces'])) {
$this->formatTraces($command, $result['traces'], $allAnalyzers);
}
$command->line("<fg=cyan>Documentation URL: <href={$result['docsUrl']}>{$result['docsUrl']}</></fg=cyan>");
}
$this->category = $result['category'];
}
/**
* Called after analysis for printing the final output.
*
* @param \Illuminate\Console\Command $command
* @param bool $allAnalyzers
* @return void
*/
public function afterAnalysis(Command $command, bool $allAnalyzers)
{
if ($allAnalyzers) {
$this->printReportCard($command);
}
}
/**
* @param \Illuminate\Console\Command $command
* @param array $traces
* @param bool $allAnalyzers
*/
protected function formatTraces(Command $command, array $traces, bool $allAnalyzers)
{
if ($command->option('details')) {
collect($traces)->each(function (Trace $trace) use ($command) {
$command->line(
"<fg=magenta>At ".$trace->relativePath().", line ".$trace->lineNumber
.(is_null($trace->details) ? "." : (": ".$trace->details))."</fg=magenta>"
);
});
return;
}
collect($traces)->groupBy(function (Trace $trace) {
return $trace->relativePath();
})->when($allAnalyzers && $this->compactLines, function ($collection) {
return $collection->take(5);
})->each(function ($traces, $path) use ($command) {
$lineNumbers = collect($traces)->map(function (Trace $trace) {
return $trace->lineNumber;
})->toArray();
$command->line(
"<fg=magenta>At ".$path.(empty($lineNumbers) ? "" : ": line(s): ")
.collect($lineNumbers)->join(', ', ' and ').".</fg=magenta>"
);
});
$count = collect($traces)->groupBy(function (Trace $trace) {
return $trace->relativePath();
})->count();
if ($count > 5 && $allAnalyzers && $this->compactLines) {
$command->line("<fg=magenta>And "
.($count - 5)
."</fg=magenta> more file(s).");
}
}
/**
* Update the result based on the analysis.
*
* @param \Illuminate\Console\Command $command
* @return string
*/
protected function printReportCard(Command $command)
{
$command->getOutput()->newLine();
$command->getOutput()->title('Report Card');
$rightAlign = (new TableStyle())->setPadType(STR_PAD_LEFT);
$command->table(
array_merge(['Status'], Enlightn::$categories, ['Total']),
collect(['passed', 'failed', 'skipped', 'error'])->map(function ($status) use ($command) {
return array_merge(
[$status === 'skipped' ? 'Not Applicable' : ucfirst($status)],
collect(array_merge(Enlightn::$categories, ['Total']))->map(function ($category) use ($status, $command) {
return $this->formatResult($status, $category, $command->result);
})->toArray()
);
})->values()->toArray(),
'default',
['default', $rightAlign, $rightAlign, $rightAlign, $rightAlign]
);
}
/**
* Get the result with percentage for each category.
*
* @param string $status
* @param string $category
* @param array $result
* @return string
*/
protected function formatResult(string $status, string $category, array $result)
{
$totalAnalyzersInCategory = (float) collect($result[$category])->filter(function ($_, $status) {
return in_array($status, ['passed', 'failed', 'skipped', 'error']);
})->sum(function ($count) {
return $count;
});
if ($totalAnalyzersInCategory == 0) {
// Avoid division by zero.
$percentage = 0;
} else {
$percentage = round((float) $result[$category][$status] * 100 / $totalAnalyzersInCategory, 0);
}
return $result[$category][$status]
.str_pad(" ({$percentage}%)", 7, " ", STR_PAD_LEFT);
}
/**
* Set the console colors for Enlightn's logo.
*
* @param \Illuminate\Console\OutputStyle $output
* @return void
*/
protected function setColors(OutputStyle $output)
{
collect([
'e' => 'green',
'n' => 'green',
'l' => 'green',
'i' => 'green',
'g' => 'green',
'h' => 'green',
't' => 'green',
'ns' => 'green',
])->each(function ($color, $tag) use ($output) {
$output->getFormatter()->setStyle($tag, new OutputFormatterStyle($color));
});
}
/**
* Get the appropriate symbol for the status.
*
* @param string $status
* @return string
*/
protected function getSymbolForStatus(string $status)
{
switch ($status) {
case 'passed':
return '<fg=green>Passed</fg=green>';
case 'failed':
return '<fg=red>Failed</fg=red>';
case 'skipped':
return '<fg=cyan>Not Applicable</fg=cyan>';
case 'error':
return '<fg=magenta>Exception</fg=magenta>';
}
return '';
}
}

View File

@@ -0,0 +1,37 @@
<?php
namespace Enlightn\Enlightn\Console\Formatters;
use Illuminate\Console\Command;
interface Formatter
{
/**
* Called before analysis for initialization or printing a greeting message.
*
* @param \Illuminate\Console\Command $command
* @return void
*/
public function beforeAnalysis(Command $command);
/**
* Called for each analyzer with the result.
*
* @param \Illuminate\Console\Command $command
* @param array $result
* @param int $current
* @param int $total
* @param bool $allAnalyzers
* @return void
*/
public function parseAnalyzerResult(Command $command, array $result, int $current, int $total, bool $allAnalyzers);
/**
* Called after analysis for printing the final output.
*
* @param \Illuminate\Console\Command $command
* @param bool $allAnalyzers
* @return void
*/
public function afterAnalysis(Command $command, bool $allAnalyzers);
}

View File

@@ -0,0 +1,12 @@
<?php
return <<<LOGO
<e>______</e><n></n> <l>___</l><i></i><g></g> <h>__</h> <t>__</t><ns></ns>
<e>/ ____/</e><n>___</n> <l>/</l> <i>(_)</i><g>___ _</g><h>/ /_</h> <t>/ /_</t><ns>____</ns>
<e>/ __/</e><n> / __ |</n><l>/ /</l> <i>/</i> <g>__ /</g> <h>__ |</h><t>/ __/</t> <ns>__ |</ns>
<e>/ /___</e><n>/ / / /</n> <l>/</l> <i>/</i> <g>/_/ /</g> <h>/ / /</h> <t>/_</t><ns>/ / / /</ns>
<e>/_____/</e><n>_/ /_/</n><l>_/</l><i>_/</i><g>\__, /</g><h>_/ /_/</h><t>\__/</t><ns>_/ /_/</ns>
<e></e><n></n><l></l><i></i> <g>/____/</g><h></h><t></t><ns></ns>
LOGO;

View File

@@ -0,0 +1,400 @@
<?php
namespace Enlightn\Enlightn;
use Enlightn\Enlightn\Analyzers\Analyzer;
use Illuminate\Support\Arr;
use Illuminate\Support\Str;
use ReflectionClass;
use Symfony\Component\Finder\Finder;
use Throwable;
class Enlightn
{
/**
* The registered analyzer instances.
*
* @var array
*/
public static $analyzerClasses = [];
/**
* The registered analyzer instances.
*
* @var array
*/
public static $analyzers = [];
/**
* The registered analyzer categories
*
* @var array
*/
public static $categories = [];
/**
* The callback to be executed after an analyzer is run.
*
* @var callable|null
*/
public static $afterCallback = null;
/**
* The callback that filters the analyzers that should be run.
*
* @var callable|null
*/
public static $filterCallback = null;
/**
* The callback to be executed before running all analyzers.
*
* @var callable|null
*/
public static $beforeRunningCallback = null;
/**
* The paths of the files to run the analysis on.
*
* @var \Illuminate\Support\Collection
*/
public static $filePaths;
/**
* Determine whether to re-throw analyzer exceptions.
*
* @var bool
*/
public static $rethrowExceptions = false;
/**
* Register the Enlightn analyzers if Enlightn is enabled.
*
* @param array $analyzerClasses
* @return void
* @throws \ReflectionException
*/
public static function register($analyzerClasses = [])
{
static::registerAnalyzers($analyzerClasses);
static::$filePaths = static::getFilesToAnalyze();
}
/**
* Run all the registered Enlightn analyzers.
*
* @param \Illuminate\Contracts\Foundation\Application $app
* @return void
* @throws \Illuminate\Contracts\Container\BindingResolutionException|\Throwable
*/
public static function run($app)
{
static::$analyzers = [];
foreach (static::$analyzerClasses as $analyzerClass) {
$analyzer = $app->make($analyzerClass);
static::$analyzers[] = $analyzer;
}
static::$analyzers = Arr::sort(static::$analyzers, function ($analyzer) {
return $analyzer->category.get_class($analyzer);
});
static::callBeforeRunningCallback();
foreach (static::$analyzers as $analyzer) {
try {
$analyzer->run($app);
} catch (Throwable $e) {
$analyzer->recordException($e);
if (static::$rethrowExceptions) {
throw $e;
}
}
static::callAfterCallback($analyzer);
}
}
/**
* Determine if Enlightn Pro is installed.
*
* @return bool
*/
public static function isPro()
{
return class_exists(\Enlightn\EnlightnPro\EnlightnProServiceProvider::class);
}
/**
* Call the after callback on the analyzer.
*
* @param \Enlightn\Enlightn\Analyzers\Analyzer $analyzer
* @return void
*/
public static function callAfterCallback(Analyzer $analyzer)
{
if (! is_null(static::$afterCallback)) {
call_user_func(static::$afterCallback, $analyzer->getInfo());
}
}
/**
* Register an after callback on the analyzer.
*
* @param callable $callback
* @return void
*/
public static function using($callback)
{
static::$afterCallback = $callback;
}
/**
* Determine whether the analyzer should run based on the filter callback.
*
* @param string $class
* @return bool
*/
public static function filter(string $class)
{
return is_null(static::$filterCallback) ? true : call_user_func(static::$filterCallback, $class);
}
/**
* Register a filter callback on the analyzer.
*
* @param callable $callback
* @return void
*/
public static function filterUsing($callback)
{
static::$filterCallback = $callback;
}
/**
* Set the filter callback to filter analyzers that should be run in CI mode.
*
* @return void
*/
public static function filterAnalyzersForCI()
{
static::filterUsing(function ($class) {
if (! empty($ciAnalyzers = config('enlightn.ci_mode_analyzers'))) {
return in_array($class, $ciAnalyzers);
}
return $class::$runInCI && ! in_array($class, config('enlightn.ci_mode_exclude_analyzers'));
});
}
/**
* Call the before running callback.
*
* @return void
*/
public static function callBeforeRunningCallback()
{
if (! is_null(static::$beforeRunningCallback)) {
call_user_func(static::$beforeRunningCallback);
}
}
/**
* Register a before running callback.
*
* @param callable $callback
* @return void
*/
public static function beforeRunning($callback)
{
static::$beforeRunningCallback = $callback;
}
/**
* Flush all the registered Enlightn analyzers and analyzer classes.
*
* @return void
*/
public static function flush()
{
static::$analyzers = [];
static::$categories = [];
static::$analyzerClasses = [];
static::$afterCallback = null;
static::$filterCallback = null;
static::$beforeRunningCallback = null;
}
/**
* Determine if a given analyzer class has been registered.
*
* @param string $class
* @return bool
*/
public static function hasAnalyzer(string $class)
{
return in_array($class, static::$analyzerClasses);
}
/**
* Determine if a given category has been registered.
*
* @param string $category
* @return bool
*/
public static function hasCategory(string $category)
{
return in_array($category, static::$categories);
}
/**
* Get a collection of the files to analyze.
*
* @return \Illuminate\Support\Collection
*/
public static function getFilesToAnalyze()
{
$paths = collect(config('enlightn.base_path', [
app_path(),
database_path('migrations'),
database_path('seeders'),
]))->filter(function ($path) {
return file_exists($path);
})->toArray();
// Paths are either all directories or all files. A mix of
// files and directories is currently not supported.
$files = collect($paths)->every(function ($value) {
return is_dir($value);
}) ? (new Finder)->in($paths)->exclude('vendor')->name('*.php')
->notName('*.blade.php')->files() : Arr::wrap($paths);
return collect($files)->map(function ($file) {
return is_string($file) ? $file : $file->getRealPath();
});
}
/**
* Get the paths of the analyzers.
*
* @return array
*/
public static function getAnalyzerPaths()
{
return collect(config('enlightn.analyzer_paths', ['Enlightn\\Enlightn\\Analyzers' => __DIR__.'/Analyzers']))
->filter(function ($dir) {
return file_exists($dir);
})->toArray();
}
/**
* Get the configured Enlightn analyzer classes.
*
* @return array
*/
public static function getAnalyzerClasses()
{
if (! in_array('*', Arr::wrap(config('enlightn.analyzers', '*')))) {
return Arr::wrap(config('enlightn.analyzers'));
}
$analyzerClasses = [];
if (empty($paths = static::getAnalyzerPaths())) {
return [];
}
collect($paths)->each(function ($path, $baseNamespace) use (&$analyzerClasses) {
$files = is_dir($path) ? (new Finder)->in($path)->files() : Arr::wrap($path);
foreach ($files as $fileInfo) {
$analyzerClass = $baseNamespace.str_replace(
['/', '.php'],
['\\', ''],
Str::after(
is_string($fileInfo) ? $fileInfo : $fileInfo->getRealPath(),
realpath($path)
)
);
$analyzerClasses[] = $analyzerClass;
}
});
if (empty($exclusions = config('enlightn.exclude_analyzers', []))) {
return $analyzerClasses;
}
return collect($analyzerClasses)->filter(function ($analyzerClass) use ($exclusions) {
return ! in_array($analyzerClass, $exclusions);
})->toArray();
}
/**
* Get the count of the number of analyzers registered.
*
* @return int
*/
public static function totalAnalyzers()
{
return (count(static::$analyzers) > 0) ? count(static::$analyzers) : count(static::$analyzerClasses);
}
/**
* Register the configured Enlightn analyzer classes.
*
* @param array $analyzerClasses
* @return void
* @throws \ReflectionException
*/
protected static function registerAnalyzers($analyzerClasses = [])
{
$analyzerClasses = empty($analyzerClasses) ? static::getAnalyzerClasses() : $analyzerClasses;
foreach ($analyzerClasses as $analyzerClass) {
static::registerAnalyzer($analyzerClass);
}
}
/**
* Register an Enlightn analyzer class.
*
* @param string $class
* @return void
* @throws \ReflectionException
*/
protected static function registerAnalyzer($class)
{
if (is_subclass_of($class, Analyzer::class) &&
! (new ReflectionClass($class))->isAbstract() &&
! static::hasAnalyzer($class) &&
static::filter($class)) {
static::$analyzerClasses[] = $class;
static::registerCategory($class);
}
}
/**
* Register an Enlightn analyzer category.
*
* @param string $class
* @return void
*/
protected static function registerCategory($class)
{
$category = get_class_vars($class)['category'];
if (! is_null($category) && ! self::hasCategory($category)) {
static::$categories[] = $category;
static::$categories = Arr::sort(static::$categories);
}
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace Enlightn\Enlightn;
use Enlightn\Enlightn\Inspection\Inspector;
use Enlightn\Enlightn\Reporting\API;
use Enlightn\Enlightn\Reporting\Client;
use Illuminate\Support\ServiceProvider;
class EnlightnServiceProvider extends ServiceProvider
{
/**
* Bootstrap any package services.
*
* @return void
*/
public function boot()
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__.'/../config/enlightn.php' => config_path('enlightn.php'),
], 'enlightn');
}
}
/**
* Register any package services.
*
* @return void
*/
public function register()
{
$this->commands([
Console\EnlightnCommand::class,
Console\BaselineCommand::class,
]);
$this->mergeConfigFrom(__DIR__.'/../config/enlightn.php', 'enlightn');
$this->app->singleton(Inspector::class);
$this->app->resolving(Inspector::class, function ($inspector) {
$inspector->start(Enlightn::$filePaths->toArray());
});
$this->app->singleton(Composer::class, function ($app) {
return new Composer($app->make('files'), $app->basePath());
});
$this->app->singleton(PHPStan::class, function ($app) {
return new PHPStan($app->make('files'), $app->basePath());
});
$this->app->afterResolving(PHPStan::class, function ($PHPStan) {
$PHPStan->start(Enlightn::$filePaths->toArray());
});
$this->app->singleton(NPM::class, function ($app) {
return new NPM($app->make('files'), $app->basePath());
});
$this->app->singleton(API::class, function ($app) {
$client = new Client(
$app->config->get('enlightn.credentials.username'),
$app->config->get('enlightn.credentials.api_token')
);
return new API($client);
});
}
}

Some files were not shown because too many files have changed in this diff Show More