zircote/swagger-php v4 upgradeでWarning: Required @OA\Info() not found
事象
Dependabotが作成するPRの対応で発生
./vendor/bin/openapi ./app/Http/ -o ./openapi.yml Warning: Required @OA\Info() not found
3系から4系へのupgrade
原因
@OA\Infoを定義しているPHPがClassでは無くFile
<?php /** * @OA\Info( * title="xxxx", * version="v1.0" * ) */
対応
Migrating to v4に書いてありました
Annotations now must be associated with either a class/trait/interface, method or property.
FileからClassに変更
app/Http/OpenApiSpec.php
<?php namespace app\Http; /** * @OA\Info( * title="xxxx", * version="v1.0" * ) */ class OpenApiSpec { }
Migration to v4のリンクが404ですが、場所を移動したみたいです
OWASP ZAP Docker user-agentを変更
はじめに
CircleCIでテスト実行時にAWS WAFにIPを追加していたが、AWS WAF v2にupgradeしたところ、IPを認識する時間が長くなったので、Allowの条件をIPからuser-agentに変更した
github.com
を見る限りはoption指定出来そう
public static final String DEFAULT_USER_AGENT = CONNECTION_BASE_KEY + ".defaultUserAgent";
方法
-z "-config connection.defaultUserAgent='任意のUA'"
を指定
例)
$ docker run -v $(pwd):/zap/wrk/:rw -t owasp/zap2docker-stable zap-baseline.py -t "https://tezu.hatenablog.com/" -z "-config connection.defaultUserAgent='app-name/1.0.0 (OWASP ZAP)'"
OWASP ZAP DockerをCircleCIで実行
はじめに
CircleCIで初めてリモートDocker(setup_remote_docker)を使った。それなりにハマったので整理
IPがプライマリとリモートで異なる
テスト対象はdevelop環境なので、AWS WAFでIP制限をしている
既にCypressでE2Eテストを実行しているので、実施前後でWAFにIP追加(削除)の実績有りだったが、IPが異なるのでハマった
WAFのIP追加はcommandsで共通化しているので、parametersにリモート有無を追加
- リモート
$DOCKER_HOST
- プライマリ コンテナ
curl -s ifconfig.me
commands: waf-add-address: parameters: is-remote: type: boolean default: false steps: - run: name: waf-add-address command: | if << parameters.is-remote >> ; then IP=`echo $DOCKER_HOST | grep -Eo '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}'` else IP=`curl -s ifconfig.me` fi IPSETID=`aws waf list-ip-sets | jq -r '.IPSets[] | select(.Name == "allow-ip-address-circleci") | .IPSetId'` TOKEN=`aws waf get-change-token | jq -r .ChangeToken` aws waf update-ip-set --ip-set-id ${IPSETID} --change-token ${TOKEN} --updates "Action="INSERT",IPSetDescriptor={Type="IPV4",Value="${IP}/32"}"
jobs: e2e-test: steps: - waf-add-address # Cypress実行 - waf-delete-address penetration-test: steps: - waf-add-address: is-remote: true # OWASP ZAP実行 - waf-delete-address: is-remote: true
環境変数が取得出来ない
CircleCI は環境変数の設定時の挿入をサポートしませんが、BASH_ENV を使用して、現在のシェルに変数を設定することは可能です。 これは、PATH を変更するときや、他の変数を参照する環境変数を設定するときに便利です。
PHPUnitでカバレッジ計測した結果をSlack通知する際に網羅率を含めて、リポートを見るトリガーにしている(100%だったらリポートを見る必要が無いので)
XMLから取得して環境変数に設定後に、Slack通知するcommandの引数に指定している
jobs: unit-test-and-report: - run: name: Run PHPUnit command: | mkdir -p reports vendor/bin/phpunit --dump-xdebug-filter xdebug-filter.php vendor/bin/phpunit --configuration phpunit.xml --prepend xdebug-filter.php --log-junit reports/junit/results.xml --coverage-html coverage --coverage-xml coverage -d memory_limit=-1 echo "export COVERAGE_PERCENT=`xmllint --xpath "string(/*[local-name()='phpunit']/*[local-name()='project']/*[local-name()='directory']/*[local-name()='totals']/*[local-name()='lines']/@percent)" coverage/index.xml`" >> $BASH_ENV - notify-slack-for-coverage: url: https://$CIRCLE_BUILD_NUM-XXXXXXXXX-gh.circle-artifacts.com/0/coverage/index.html percent: $COVERAGE_PERCENT
同じ要領でリモートDockerのIPを取得して、環境変数に設定したがcommands側で取得出来ない。CIDR表記不正でエラー
上述の通り、$DOCKER_HOST
からIPを取得で解決
/32' at 'updates.1.member.iPSetDescriptor.value' failed to satisfy constraint: Member must satisfy regular expression pattern: .*\S.*
commands: waf-add-address: parameters: ip: type: string steps: - run: name: waf-add-address command: | IPSETID=`aws waf list-ip-sets | jq -r '.IPSets[] | select(.Name == "allow-ip-address-circleci") | .IPSetId'` TOKEN=`aws waf get-change-token | jq -r .ChangeToken` aws waf update-ip-set --ip-set-id ${IPSETID} --change-token ${TOKEN} --updates "Action="INSERT",IPSetDescriptor={Type="IPV4",Value="<< parameters.ip >>/32"}" jobs: penetration-test: steps: - run: name: Set IP Address command: | echo "export IP_ADDR=`docker run -t owasp/zap2docker-stable curl inet-ip.info`" >> $BASH_ENV - waf-add-address: ip: $IP_ADDR
OWASP ZAPアラート有りでCircleCIのJobがFail
OWASP ZAPでアラートがある場合はstatusが0以外となりJobがFailする
後続Jobを実行することは when: always
指定で回避出来るが、CircleCI管理コンソールで実行結果を見た際に真っ赤になるので、exit 0
としたい
FAIL-NEW: 0 FAIL-INPROG: 0 WARN-NEW: 1 WARN-INPROG: 0 INFO: 0 IGNORE: 0 PASS: 37 Exited with code exit status 2 CircleCI received exit code 2
-I do not return failure on warning
-Iを指定することで回避
コードを見て理解できました
https://github.com/zaproxy/zaproxy/blob/main/docker/zap-baseline.py#L443
https://github.com/zaproxy/zaproxy/blob/main/docker/zap-baseline.py#L646
shell: /bin/sh
や set +e
はダメでした
リモートDockerのボリュームをマウントできない
ジョブ空間からリモート Docker 内のコンテナにボリュームをマウントすること (およびその逆) はできません。
SSHで直接実行するとNGのケースでも動作したので少しハマった
NG
jobs: penetration-test: - run: name: Run OWASP ZIP working_directory: ./test-penetration command: | docker run -u root -v $(pwd):/zap/wrk/:rw -t owasp/zap2docker-stable zap-baseline.py -t "テスト対象のURL" -I -r owasp-report.html -J owasp-report.json - run: name: copy report remote to local working_directory: ./test-penetration command: | docker run -u root -v $(pwd):/zap/wrk/:rw -t owasp/zap2docker-stable cat /zap/wrk/project/test-penetration/owasp-report.html > owasp-report.html docker run -u root -v $(pwd):/zap/wrk/:rw -t owasp/zap2docker-stable cat /zap/wrk/project/test-penetration/owasp-report.json > owasp-report.json
OK
jobs: penetration-test: - run: name: Run OWASP ZIP working_directory: ./test-penetration command: | docker create -v /zap/wrk --name remote-owasp alpine:3.4 /bin/true docker run -u root --volumes-from remote-owasp owasp/zap2docker-stable zap-baseline.py -t "テスト対象のURL" -I -r owasp-report.html -J owasp-report.json - run: name: copy report remote to local working_directory: ./test-penetration command: | docker cp remote-owasp:/zap/wrk/owasp-report.html owasp-report.html docker cp remote-owasp:/zap/wrk/owasp-report.json owasp-report.json
Laravel Validation Test Requestクラスのみを対象
はじめに
バリデーションのテストをしたいのですが、 テスト 5.2 Laravel だと、bladeファイルも必要なので ちょっと違うんですよね
テスト対象
一からコードを書くのが面倒なので、以下を購入してイジって遊んでます
BlogRequestはjosh adminのコード
<?php namespace App\Http\Requests; use App\Http\Requests\Request; class BlogRequest extends Request { /** * Determine if the user is authorized to make this request. * * @return bool */ public function authorize() { return true; } /** * Get the validation rules that apply to the request. * * @return array */ public function rules() { return [ 'title' => 'required|min:3', 'content' => 'required|min:3', 'blog_category_id' => 'required', ]; } }
テストケース
サービスコンテナにMockのリクエストをセットするのがポイント
<?php use App\Http\Requests\BlogRequest; use Illuminate\Http\Exception\HttpResponseException; use Illuminate\Http\Request; class BlogRequestTest extends TestCase { // php artisan make:test App\Http\Requests\BlogRequestTest /** * @return void * @test */ public function 未入力() { try { // App::makeでvalidationが実行されてしまう。。 App::make(BlogRequest::class); } catch (HttpResponseException $e) { $messages = $this->getMessages(); $this->assertEquals('The title field is required.', $messages['title'][0]); $this->assertEquals('The content field is required.', $messages['content'][0]); $this->assertEquals('The blog category id field is required.', $messages['blog_category_id'][0]); } catch (Exception $e) { $this->fail("HttpResponseExceptionが発生すべき"); } } /** * @return void * @test */ public function 最小値_直前の値() { $this->bindRequest([ 'title' => '11', 'content' => '22', 'blog_category_id' => 1 ]); try { App::make(BlogRequest::class); } catch (HttpResponseException $e) { $messages = $this->getMessages(); $this->assertEquals('The title must be at least 3 characters.', $messages['title'][0]); $this->assertEquals('The content must be at least 3 characters.', $messages['content'][0]); $this->assertFalse(isset($messages['blog_category_id'])); } catch (Exception $e) { $this->fail("HttpResponseExceptionが発生すべき"); } } /** * @return void * @test */ public function 最小値() { $this->bindRequest([ 'title' => '111', 'content' => '222', 'blog_category_id' => 1 ]); try { App::make(BlogRequest::class); } catch (HttpResponseException $e) { $this->fail("全て正常値。HttpResponseExceptionは発生しない"); } catch (Exception $e) { $this->fail("全て正常値。Exceptionは発生しない"); } } /** * @param $params array */ private function bindRequest($params) { // vendor/laravel/framework/src/Illuminate/Foundation/Providers/FoundationServiceProvider.php // でサービスコンテナからrequestを取り出してRequestクラスにパラメータをセットしている // // $this->initializeRequest($request, $app['request']); // // protected function initializeRequest(FormRequest $form, Request $current) // $form->initialize( // $current->query->all(), $current->request->all(), $current->attributes->all(), // $current->cookies->all(), $files, $current->server->all(), $current->getContent() // ); // サービスコンテナにMockのリクエストをセット App::instance('request', Request::create('', 'POST', $params, [], [], [], [])); } /** * @return array */ private function getMessages() { /** * @var \Illuminate\Support\ViewErrorBag $viewErrorBag * @var \Illuminate\Support\MessageBag $messageBag */ $viewErrorBag = Session::get('errors'); $messageBag = $viewErrorBag->getBag('default'); return $messageBag->getMessages(); } }
Laravel Monolog Cascadeを使用してログレベル毎に出力するファイルを変更する
概要
ログレベル毎に出力するファイルを変更したい
Laravel標準ではdailyでログファイルをローテートは出来る程度
.env
APP_LOG=daily
storage/logs
laravel-2016-10-04.log laravel-2016-10-05.log laravel-2016-10-06.log
Monolog Cascadeを使用して、ログレベル毎に出力するファイルを変更する github.com
手順
インストール
composer.jsonに以下を追記後、composer updateを実行
"require": { "theorchard/monolog-cascade": "^0.4.0" }
設定
https://github.com/theorchard/monolog-cascade Configuration structureを参考に作成
config/monolog.php
<?php return [ 'version' => 1, 'formatters' => [ 'dashed' => [ 'format' => "%datetime%-%channel%.%level_name% - %message% %context% %extra%\n", 'include_stacktraces' => true ], ], 'handlers' => [ //------------------------------------------------- // Admin // ------------------------------------------------ 'admin_info' => [ 'class' => 'Monolog\Handler\RotatingFileHandler', 'level' => 'INFO', 'filename' => env('LOG_ADMIN_INFO_PATH', storage_path('logs/admin_info.log')), 'maxFiles' => 5, 'formatter' => 'dashed', 'processors' => ['web_processor', 'introspection_processor', 'login_user_processor'], ], 'admin_error' => [ 'class' => 'Monolog\Handler\RotatingFileHandler', 'level' => 'ERROR', 'filename' => env('LOG_ADMIN_ERROR_PATH', storage_path('logs/admin_error.log')), 'maxFiles' => 5, 'formatter' => 'dashed', 'processors' => ['web_processor', 'introspection_processor', 'login_user_processor'], ], // WARNINGのみ出力 '_admin_warning' => [ 'class' => 'Monolog\Handler\RotatingFileHandler', 'level' => 'WARNING', 'filename' => env('LOG_ADMIN_WARNING_PATH', storage_path('logs/admin_warning.log')), 'maxFiles' => 5, 'formatter' => 'dashed', 'processors' => ['web_processor', 'introspection_processor', 'login_user_processor'], ], 'admin_warning' => [ 'class' => 'Monolog\Handler\FilterHandler', 'handler' => '_admin_warning', 'minLevelOrList' => ['WARNING'], ], //------------------------------------------------- // Front // ------------------------------------------------ 'front_info' => [ 'class' => 'Monolog\Handler\RotatingFileHandler', 'level' => 'INFO', 'filename' => env('LOG_FRONT_INFO_PATH', storage_path('logs/front_info.log')), 'maxFiles' => 5, 'formatter' => 'dashed', 'processors' => ['web_processor', 'introspection_processor'], ], 'front_error' => [ 'class' => 'Monolog\Handler\RotatingFileHandler', 'level' => 'ERROR', 'filename' => env('LOG_FRONT_ERROR_PATH', storage_path('logs/front_error.log')), 'maxFiles' => 5, 'formatter' => 'dashed', 'processors' => ['web_processor', 'introspection_processor'], ], // WARNINGのみ出力 '_front_warning' => [ 'class' => 'Monolog\Handler\RotatingFileHandler', 'level' => 'WARNING', 'filename' => env('LOG_FRONT_WARNING_PATH', storage_path('logs/front_warning.log')), 'maxFiles' => 5, 'formatter' => 'dashed', 'processors' => ['web_processor', 'introspection_processor'], ], 'front_warning' => [ 'class' => 'Monolog\Handler\FilterHandler', 'handler' => '_front_warning', 'minLevelOrList' => ['WARNING'], ], ], 'processors' => [ // IP,URL,HTTP METHOD,referrer 'web_processor' => [ 'class' => 'Monolog\Processor\WebProcessor' ], // 出力ファイル名、出力行 'introspection_processor' => [ 'class' => 'Monolog\Processor\IntrospectionProcessor', 'skipClassesPartials' => [ 'Monolog\\', 'Illuminate\\', ], ], // ログイン情報 'login_user_processor' => [ 'class' => 'App\Services\Monolog\Processor\LoginUserProcessor' ], ], 'loggers' => [ // このキーをMonologSetting::change()で指定 'admin' => [ 'handlers' => ['admin_info', 'admin_error', 'admin_warning'] ], 'front' => [ 'handlers' => ['front_info', 'front_error', 'front_warning'] ] ] ];
Monolog Cascadeに差し替え
LaravelのMonolog Loggerと差し替える
app/Services/Monolog/MonologSetting.php
<?php namespace App\Services\Monolog; use Cascade\Cascade; use Illuminate\Log\Writer; use Log; class MonologSetting { /** * ログ設定を変更する * @param string $logName 変更名 config/monolog.phpのhandlersキー */ public static function change($logName) { // 登録済みインスタンスを削除 Log::clearResolvedInstances(); app()->instance('log', new Writer(Cascade::getLogger($logName))); } }
Laravelの登録方法を参考にした
<?php namespace Illuminate\Foundation\Bootstrap; use Illuminate\Log\Writer; use Monolog\Logger as Monolog; use Illuminate\Contracts\Foundation\Application; class ConfigureLogging { /** * Register the logger instance in the container. * * @param \Illuminate\Contracts\Foundation\Application $app * @return \Illuminate\Log\Writer */ protected function registerLogger(Application $app) { $app->instance('log', $log = new Writer( new Monolog($app->environment()), $app['events']) ); return $log; } }
Facadeを追加
<?php namespace App\Facades; use Illuminate\Support\Facades\Facade; class MonologSetting extends Facade { protected static function getFacadeAccessor() { return 'monolog_setting'; } }
config\app.php
<?php return [ 'aliases' => [ 'MonologSetting' => App\Facades\MonologSetting::class, // ここを追加 ], ];
サービスプロバイダー
MonologSettingのバインドとFront/Admin/コンソールでロガーを切替
artisanコマンドで雛型作成
php artisan make:provider MonologSettingServiceProvider Provider created successfully.
<?php namespace App\Providers; use Illuminate\Support\ServiceProvider; use MonologSetting; use Cascade\Cascade; class MonologSettingServiceProvider extends ServiceProvider { /** * Bootstrap the application services. * * @return void */ public function boot() { Cascade::fileConfig(config_path('monolog.php')); $changeName = null; // コマンド実行時とwebアクセス時で設定を変更 if (php_sapi_name() == 'cli') { $changeName = 'console'; } elseif (str_is('/admin/*', env('REQUEST_URI'))) { $changeName = 'admin'; } if (!is_null($changeName)) { MonologSetting::change($changeName); } } /** * Register the application services. * * @return void */ public function register() { // キーはMonologSettingファサードのgetFacadeAccessor()の戻り値と一致 \App::bind('monolog_setting', function() { return new \App\MonologSetting; }); } }
config\app.php
'providers' => [
/*
* Application Service Providers...
*/
App\Providers\AppServiceProvider::class,
App\Providers\AuthServiceProvider::class,
App\Providers\EventServiceProvider::class,
App\Providers\RouteServiceProvider::class,
App\Providers\MonologSettingServiceProvider::class, // add
],
ログインユーザ名を出力
認証認可はSentinel Manual :: Cartalystを使用
<?php namespace App\Services\Monolog\Processor; use Sentinel; class LoginUserProcessor { /** * @param array $record * @return array */ public function __invoke(array $record) { if (Sentinel::check()) { $user = Sentinel::getUser(); $record['extra']['username'] = $user->getUserLogin(); } return $record; } }
結果
変更前
storage/logs/laravel-2016-10-13.log
[2016-10-13 07:02:52] local.WARNING: array ( '_method' => 'PUT', '_token' => 'E5ola1t7HaX79kfkEcZW3oNxhdYau10SdxDUuBDc', 'first_name' => '', 'last_name' => '', 'email' => 'admin@xxxxx.xxx', 'password' => 'xxxxx', 'password_confirm' => '', ) [2016-10-13 07:02:52] local.WARNING: array ( 'first_name' => array ( 0 => 'The first name field is required.', ), 'last_name' => array ( 0 => 'The last name field is required.', ), 'email' => array ( 0 => 'The email has already been taken.', ), ) [2016-10-13 07:05:51] local.ERROR: exception 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException' in \vendor\laravel\framework\src\Illuminate\Routing\RouteCollection.php:161 Stack trace: #0 \vendor\laravel\framework\src\Illuminate\Routing\Router.php(821): Illuminate\Routing\RouteCollection->match(Object(Illuminate\Http\Request))
変更後
errorレベル
storage/logs/front_error-2016-10-13.log
2016-10-13 07:31:04-front.ERROR - exception 'Symfony\Component\HttpKernel\Exception\NotFoundHttpException' in vendor\laravel\framework\src\Illuminate\Routing\RouteCollection.php:161 Stack trace: #0 vendor\laravel\framework\src\Illuminate\Routing\Router.php(821): Illuminate\Routing\RouteCollection->match(Object(Illuminate\Http\Request))
warnレベル
storage/logs/front_warning-2016-10-13.log
2016-10-13 07:31:45-front.WARNING - array ( '_method' => 'PUT', '_token' => 'E5ola1t7HaX79kfkEcZW3oNxhdYau10SdxDUuBDc', 'first_name' => '', 'last_name' => '', 'email' => 'admin@xxxxx.xxx', 'password' => 'xxxxx', 'password_confirm' => '', ) 2016-10-13 07:31:45-front.WARNING - array ( 'first_name' => array ( 0 => 'The first name field is required.', ), 'last_name' => array ( 0 => 'The last name field is required.', ), 'email' => array ( 0 => 'The email has already been taken.', ), )