├── priv └── .gitkeep ├── config ├── sys.config └── vm.args ├── rebar3 ├── .gitignore ├── apps └── ct_advisor │ ├── src │ ├── ct_advisor.app.src │ ├── db_connect.erl │ ├── ct_advisor_app.erl │ ├── ct_advisor_sup.erl │ ├── ct_fetch.erl │ ├── scheduler.erl │ ├── domain_parse.erl │ ├── leaf_parse.erl │ ├── ct_mail_alert.erl │ └── leaf_iterate.erl │ └── include │ └── test_constants.hrl ├── rebar.config ├── LICENSE ├── rebar.lock ├── elvis.config ├── test └── ct_advisor_SUITE.erl └── README.md /priv/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /config/sys.config: -------------------------------------------------------------------------------- 1 | [ 2 | {'ct_advisor', []} 3 | ]. 4 | -------------------------------------------------------------------------------- /rebar3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/technion/ct_advisor/HEAD/rebar3 -------------------------------------------------------------------------------- /config/vm.args: -------------------------------------------------------------------------------- 1 | -sname ct_advisor 2 | 3 | -setcookie ct_advisor_cookie 4 | 5 | +K true 6 | +A30 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .rebar3 2 | _* 3 | .eunit 4 | *.o 5 | *.beam 6 | *.plt 7 | *.swp 8 | *.swo 9 | .erlang.cookie 10 | ebin 11 | log 12 | erl_crash.dump 13 | .rebar 14 | _rel 15 | _deps 16 | _plugins 17 | _tdeps 18 | logs 19 | _build 20 | priv/credentials.rr 21 | -------------------------------------------------------------------------------- /apps/ct_advisor/src/ct_advisor.app.src: -------------------------------------------------------------------------------- 1 | {application, 'ct_advisor', 2 | [{description, "An OTP application"}, 3 | {vsn, "0.1.0"}, 4 | {registered, []}, 5 | {mod, {'ct_advisor_app', []}}, 6 | {applications, 7 | [kernel, 8 | ssl, 9 | stdlib, 10 | ibrowse, 11 | jiffy, 12 | epgsql, 13 | pgapp, 14 | gen_smtp, 15 | lager 16 | ]}, 17 | {env,[]}, 18 | {modules, [ ]}, 19 | {contributors, []}, 20 | {licenses, []}, 21 | {links, []} 22 | ]}. 23 | -------------------------------------------------------------------------------- /apps/ct_advisor/src/db_connect.erl: -------------------------------------------------------------------------------- 1 | -module(db_connect). 2 | -export([db_connect/0]). 3 | 4 | -record(credentials, {hostname, username, password}). 5 | 6 | %% Establishes a connection to Postgresql. 7 | db_connect() -> 8 | {ok, Config} = file:consult("priv/credentials.rr"), 9 | Creds = proplists:get_value(database, Config), 10 | {ok, C} = pgapp:connect([{size, 5}, 11 | {host, Creds#credentials.hostname}, 12 | {username, Creds#credentials.username}, 13 | {password, Creds#credentials.password}, 14 | {database, "ct_advisor_int_live"}]), 15 | lager:info("Connection to database started: ~p", [C]), 16 | ok. 17 | 18 | -ifdef(TEST). 19 | -include_lib("eunit/include/eunit.hrl"). 20 | -include("test_constants.hrl"). 21 | 22 | db_connect_test() -> 23 | application:ensure_all_started(pgapp), 24 | db_connect(), 25 | application:stop(pgapp). 26 | 27 | -endif. 28 | 29 | 30 | -------------------------------------------------------------------------------- /apps/ct_advisor/src/ct_advisor_app.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc ct_advisor public API 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module('ct_advisor_app'). 7 | 8 | -behaviour(application). 9 | 10 | %% Application callbacks 11 | -export([start/2, stop/1]). 12 | 13 | %%==================================================================== 14 | %% API 15 | %%==================================================================== 16 | start(_StartType, _StartArgs) -> 17 | db_connect:db_connect(), 18 | lager:set_loglevel(lager_console_backend, notice), % Debugging 19 | scheduler:start_link(), 20 | 'ct_advisor_sup':start_link(). 21 | 22 | %%-------------------------------------------------------------------- 23 | stop(_State) -> 24 | lager:info("Connection shutting down", []), 25 | ok. 26 | 27 | %%==================================================================== 28 | %% Internal functions 29 | %%==================================================================== 30 | -------------------------------------------------------------------------------- /apps/ct_advisor/src/ct_advisor_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %% @doc ct_advisor top level supervisor. 3 | %% @end 4 | %%%------------------------------------------------------------------- 5 | 6 | -module('ct_advisor_sup'). 7 | 8 | -behaviour(supervisor). 9 | 10 | %% API 11 | -export([start_link/0]). 12 | 13 | %% Supervisor callbacks 14 | -export([init/1]). 15 | 16 | -define(SERVER, ?MODULE). 17 | 18 | %%==================================================================== 19 | %% API functions 20 | %%==================================================================== 21 | 22 | start_link() -> 23 | supervisor:start_link({local, ?SERVER}, ?MODULE, []). 24 | 25 | %%==================================================================== 26 | %% Supervisor callbacks 27 | %%==================================================================== 28 | 29 | %% Child :: {Id,StartFunc,Restart,Shutdown,Type,Modules} 30 | init([]) -> 31 | {ok, { {one_for_all, 0, 1}, []} }. 32 | 33 | %%==================================================================== 34 | %% Internal functions 35 | %%==================================================================== 36 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | {erl_opts, [debug_info, {parse_transform, lager_transform}]}. 2 | {deps, [ 3 | {ibrowse, {git, "https://github.com/cmullaparthi/ibrowse.git", {tag, "v4.4.2"}}}, 4 | {jiffy, {git, "https://github.com/davisp/jiffy.git", {tag, "1.1.1"}}}, 5 | {lager, {git, "https://github.com/erlang-lager/lager.git", {tag, "3.9.2"}}}, 6 | {pgapp, {git, "https://github.com/technion/pgapp.git", "master"}}, 7 | {meck, {git, "https://github.com/eproxus/meck.git", {tag, "0.9.2"}}}, 8 | {gen_smtp, {git, "https://github.com/Vagabond/gen_smtp.git", {tag, "1.1.1"}}} 9 | ] 10 | }. 11 | {overrides, 12 | [{override, jiffy, [ 13 | {plugins, [ 14 | {pc, {git, "git@github.com:blt/port_compiler.git", {branch, "master"}}} 15 | ]}, 16 | 17 | {provider_hooks, [ 18 | {post, 19 | [ 20 | {compile, {pc, compile}}, 21 | {clean, {pc, clean}} 22 | ] 23 | }] 24 | } 25 | ]} 26 | ]}. 27 | 28 | {relx, [{release, {'ct_advisor', "0.1.0"}, 29 | ['ct_advisor', 30 | sasl]}, 31 | 32 | {sys_config, "./config/sys.config"}, 33 | {vm_args, "./config/vm.args"}, 34 | {overlay, [{mkdir, "priv"}, {copy, "priv/credentials.rr", "priv/"} ]}, 35 | {dev_mode, true}, 36 | {include_erts, false}, 37 | 38 | {extended_start_script, true}] 39 | }. 40 | 41 | {profiles, [{prod, [{relx, [{dev_mode, false}, 42 | {include_erts, true}]}] 43 | }] 44 | }. 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015, Technion . 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are 6 | met: 7 | 8 | * Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | * Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | * The names of its contributors may not be used to endorse or promote 16 | products derived from this software without specific prior written 17 | permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /rebar.lock: -------------------------------------------------------------------------------- 1 | {"1.1.0", 2 | [{<<"epgsql">>, 3 | {git,"https://github.com/epgsql/epgsql.git", 4 | {ref,"b3138f76759750ab5e6202766cb39acd88c8ff54"}}, 5 | 0}, 6 | {<<"gen_smtp">>, 7 | {git,"https://github.com/Vagabond/gen_smtp.git", 8 | {ref,"410557a4b52cbabc99062e2196fa0ec16c3c03a7"}}, 9 | 0}, 10 | {<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.9">>},1}, 11 | {<<"hut">>,{pkg,<<"hut">>,<<"1.3.0">>},1}, 12 | {<<"ibrowse">>, 13 | {git,"https://github.com/cmullaparthi/ibrowse.git", 14 | {ref,"24cb21f9ec97ab3b181617d966e180ef519c5eef"}}, 15 | 0}, 16 | {<<"jiffy">>, 17 | {git,"https://github.com/davisp/jiffy.git", 18 | {ref,"9ea1b35b6e60ba21dfd4adbd18e7916a831fd7d4"}}, 19 | 0}, 20 | {<<"lager">>, 21 | {git,"https://github.com/erlang-lager/lager.git", 22 | {ref,"459a3b2cdd9eadd29e5a7ce5c43932f5ccd6eb88"}}, 23 | 0}, 24 | {<<"meck">>, 25 | {git,"https://github.com/eproxus/meck.git", 26 | {ref,"5aaa24886db404f995c9a91b421367f6bfe6e566"}}, 27 | 0}, 28 | {<<"pgapp">>, 29 | {git,"https://github.com/technion/pgapp.git", 30 | {ref,"7d0a6e9f915f966796d66765bfaf29a3878368bc"}}, 31 | 0}, 32 | {<<"poolboy">>, 33 | {git,"git://github.com/devinus/poolboy.git", 34 | {ref,"29be47db8c2be38b18c908e43a80ebb7b9b6116b"}}, 35 | 1}, 36 | {<<"ranch">>,{pkg,<<"ranch">>,<<"1.7.1">>},1}]}. 37 | [ 38 | {pkg_hash,[ 39 | {<<"goldrush">>, <<"F06E5D5F1277DA5C413E84D5A2924174182FB108DABB39D5EC548B27424CD106">>}, 40 | {<<"hut">>, <<"71F2F054E657C03F959CF1ACC43F436EA87580696528CA2A55C8AFB1B06C85E7">>}, 41 | {<<"ranch">>, <<"6B1FAB51B49196860B733A49C07604465A47BDB78AA10C1C16A3D199F7F8C881">>}]} 42 | ]. 43 | -------------------------------------------------------------------------------- /apps/ct_advisor/src/ct_fetch.erl: -------------------------------------------------------------------------------- 1 | -module(ct_fetch). 2 | -export([fetch_entry/2, fetch_sth/0, parse_sth/1]). 3 | 4 | %% Test suits are based on the originally monitored pilot log 5 | -ifdef(TEST). 6 | -define(CT_LOG, "https://ct.googleapis.com/pilot"). 7 | -else. 8 | -define(CT_LOG, "https://ct.googleapis.com/logs/xenon2022"). 9 | -endif. 10 | 11 | -define(USERAGENT, "CTAdvisor"). 12 | 13 | %% Connect to the REST API and fetch the JSON entry for a particular node 14 | -spec fetch_entry(pos_integer(), pos_integer()) -> any(). 15 | fetch_entry(X, Y) -> 16 | Xi = integer_to_list(X), 17 | Yi = integer_to_list(Y), 18 | URL = ?CT_LOG ++ "/ct/v1/get-entries?start=" ++ Xi ++ "&end=" ++ Yi, 19 | {ok, "200", _Headers, Content} = 20 | ibrowse:send_req(URL, [{"User-Agent",?USERAGENT}], get), 21 | Content. 22 | 23 | %% Connect to the REST API and obtain the "sth", which is the total count 24 | %% of certificates currently logged with the monitor. 25 | -spec fetch_sth() -> any(). 26 | fetch_sth() -> 27 | {ok, "200", _Headers, Content} = 28 | ibrowse:send_req(?CT_LOG ++ "/ct/v1/get-sth", [{"User-Agent",?USERAGENT}], get), 29 | Content. 30 | 31 | %% Extract the sth counter from the JSON response 32 | -spec parse_sth(_) -> any(). 33 | parse_sth(JSON) -> 34 | {STH} = jiffy:decode(JSON), 35 | proplists:get_value(<<"tree_size">>, STH). 36 | 37 | -ifdef(TEST). 38 | -include_lib("eunit/include/eunit.hrl"). 39 | -include("test_constants.hrl"). 40 | 41 | %Tests based upon ID 10502585 - lolware.net 42 | fetch_entry_test() -> 43 | ibrowse:start(), 44 | ?assertEqual(ct_fetch:fetch_entry(10502585, 10502585), ?TEST_LEAF_ENTRY). 45 | 46 | fetch_sth_test() -> 47 | ibrowse:start(), 48 | fetch_sth(). 49 | 50 | parse_sth_test() -> 51 | ?assertEqual(parse_sth(?TEST_STH), 9910235). 52 | -endif. 53 | 54 | -------------------------------------------------------------------------------- /elvis.config: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | elvis, 4 | [ 5 | {config, 6 | [#{dirs => ["apps/ct_advisor/src"], 7 | filter => "*.erl", 8 | rules => [ {elvis_style, no_tabs}, 9 | {elvis_style, no_trailing_whitespace}, 10 | {elvis_style, macro_names}, 11 | %{elvis_style, macro_module_names}, 12 | {elvis_style, operator_spaces, #{rules => [{right, ","}, 13 | {right, "++"}, 14 | {left, "++"}]}}, 15 | {elvis_style, nesting_level, #{level => 3}}, 16 | {elvis_style, god_modules, #{limit => 25}}, 17 | {elvis_style, no_if_expression}, 18 | {elvis_style, invalid_dynamic_call, #{ignore => [elvis]}}, 19 | {elvis_style, used_ignored_variable}, 20 | {elvis_style, no_behavior_info}, 21 | { 22 | elvis_style, 23 | module_naming_convention, 24 | #{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$", 25 | ignore => []} 26 | }, 27 | {elvis_style, state_record_and_type}, 28 | {elvis_style, no_spec_with_records}, 29 | {elvis_style, dont_repeat_yourself, #{min_complexity => 10}} 30 | ] 31 | }, 32 | #{dirs => ["."], 33 | filter => "Makefile", 34 | rules => [{elvis_project, no_deps_master_erlang_mk, #{ignore => []}}, 35 | {elvis_project, git_for_deps_erlang_mk, #{ignore => []}}] 36 | }, 37 | #{dirs => ["."], 38 | filter => "elvis.config", 39 | rules => [{elvis_project, old_configuration_format}] 40 | } 41 | ] 42 | } 43 | ] 44 | } 45 | ]. 46 | -------------------------------------------------------------------------------- /apps/ct_advisor/src/scheduler.erl: -------------------------------------------------------------------------------- 1 | -module(scheduler). 2 | -behaviour(gen_server). 3 | -define(SERVER, ?MODULE). 4 | 5 | %% ------------------------------------------------------------------ 6 | %% API Function Exports 7 | %% ------------------------------------------------------------------ 8 | 9 | -export([start_link/0]). 10 | 11 | %% ------------------------------------------------------------------ 12 | %% gen_server Function Exports 13 | %% ------------------------------------------------------------------ 14 | 15 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, 16 | terminate/2, code_change/3]). 17 | 18 | %% ------------------------------------------------------------------ 19 | %% API Function Definitions 20 | %% ------------------------------------------------------------------ 21 | 22 | start_link() -> 23 | gen_server:start_link({local, ?SERVER}, ?MODULE, [], []). 24 | 25 | %% ------------------------------------------------------------------ 26 | %% gen_server Function Definitions 27 | %% ------------------------------------------------------------------ 28 | -define(INTERVAL, 2000). 29 | 30 | init(_Args) -> 31 | erlang:send_after(?INTERVAL, self(), scheduler), 32 | % This hack just gives us a valid PID to set as the first State 33 | Pid = spawn(fun() -> lager:debug("Initializing scheduler") end), 34 | {ok, Pid}. 35 | 36 | handle_call(_Request, _From, State) -> 37 | {reply, ok, State}. 38 | 39 | handle_cast(_Msg, State) -> 40 | {noreply, State}. 41 | 42 | handle_info(scheduler, State) -> 43 | Pid = case process_info(State) of 44 | undefined -> 45 | spawn(leaf_iterate, scheduled_check, []); 46 | _ -> 47 | lager:debug("Process ~p already running, sleeping", [State]), 48 | State 49 | end, 50 | erlang:send_after(?INTERVAL, self(), scheduler), 51 | {noreply, Pid}; 52 | 53 | handle_info(_Info, State) -> 54 | {noreply, State}. 55 | 56 | terminate(_Reason, _State) -> 57 | ok. 58 | 59 | code_change(_OldVsn, State, _Extra) -> 60 | {ok, State}. 61 | 62 | %% ------------------------------------------------------------------ 63 | %% Internal Function Definitions 64 | %% ------------------------------------------------------------------ 65 | 66 | -------------------------------------------------------------------------------- /apps/ct_advisor/src/domain_parse.erl: -------------------------------------------------------------------------------- 1 | -module(domain_parse). 2 | -export([cert_domain_list/1]). 3 | 4 | % A list of certificates with a list of names 5 | -spec cert_domain_list([any()]) -> 'ok'. 6 | cert_domain_list(Domains) -> 7 | lager:info("Domain list for review: ~p" ,[Domains]), 8 | lists:foreach(fun(X) -> per_cert_domains(X) end, Domains). 9 | 10 | % A list of domains for an individual certificate 11 | -spec per_cert_domains([{'dNSName', _}]) -> 'ok'. 12 | per_cert_domains(DomainsID) -> 13 | ID = proplists:lookup(serial, DomainsID), 14 | Domains = proplists:delete(serial, DomainsID), 15 | case lists:flatten(lookup_name_list(Domains)) of 16 | [] -> 17 | ok; 18 | Alerts -> 19 | ct_mail_alert:send_alert(Alerts, Domains, ID), 20 | ok 21 | end. 22 | 23 | %% For a given domain name - check if it's registered in the datbaase 24 | -spec lookup_name_list([{atom(), _}]) -> [[] | {_, _}]. 25 | lookup_name_list([]) -> 26 | []; 27 | 28 | lookup_name_list([{dNSName, Name}|Tail]) -> 29 | {ok, _Columns, Rows} = pgapp:equery("SELECT email FROM registrations " 30 | "WHERE ($1 LIKE concat('%.',domain) or $1 = domain) AND active =1", 31 | [Name]), 32 | Match = case Rows of 33 | [{User}] -> 34 | {Name, binary_to_list(User)}; 35 | _ -> 36 | [] 37 | end, 38 | [Match|lookup_name_list(Tail)]; 39 | 40 | lookup_name_list([{_, _Name}|Tail]) -> 41 | %TIL: There are other types of subject names - see test suite 42 | [lookup_name_list(Tail)]. 43 | 44 | -ifdef(TEST). 45 | -include_lib("eunit/include/eunit.hrl"). 46 | -include("test_constants.hrl"). 47 | lookup_fixture_test_() -> 48 | {setup, fun connect/0, fun teardown/1, fun lookup_name_listi/0}. 49 | 50 | connect() -> 51 | application:ensure_all_started(pgapp), 52 | db_connect:db_connect(), 53 | ok. 54 | 55 | teardown(_C) -> 56 | application:stop(pgapp). 57 | 58 | lookup_name_listi() -> 59 | % using lists:flatten/1 because it is always called this way 60 | ?assertEqual(lists:flatten(lookup_name_list([])), []), 61 | ?assertEqual([], lists:flatten(lookup_name_list(?TEST_NONDNS_DOMAINS))), 62 | ?assertEqual([{"lolware.net","technion@lolware.net"}, 63 | {"www.lolware.net","technion@lolware.net"}], 64 | lists:flatten(lookup_name_list(?TEST_LOOKUP_DOMAINS))). 65 | -endif. 66 | -------------------------------------------------------------------------------- /test/ct_advisor_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(ct_advisor_SUITE). 2 | -include_lib("common_test/include/ct.hrl"). 3 | %-compile(export_all). 4 | -export([all/0, init_per_suite/1, end_per_suite/1]). 5 | -export([no_update/1, update/1, domain_check/1]). 6 | 7 | all() -> [no_update, update, domain_check]. 8 | 9 | querymock([]) -> 10 | {ok, "Columns", [{10502585}]}; 11 | querymock([A]) when is_integer(A) -> 12 | {ok, 1}; 13 | querymock([_A]) -> 14 | {ok, "Columns", [{<<"technion@lolware.net">>}]}. 15 | 16 | init_per_suite(Config) -> 17 | application:start(ssl), 18 | application:start(ibrowse), 19 | Config. 20 | 21 | no_update(_Config) -> 22 | K = "{\"tree_size\":10502585,\"timestamp\":1449817937400,\"sha256_root_hash\":\"qeBs0XUYqtWTMYTEbnIKQhQefv5eOCl+dZCFwPrpljk=\",\"tree_head_signature\":\"BAMARzBFAiEA5xzKR86R2jWkX67PBabhg1/v4GrfeeBEbK4bT4Npns0CIF9ew7he6hpMwbfsNDbZOnrzByo4EQcArov1jHQFBG0K\"}", 23 | meck:new(pgapp, [non_strict]), 24 | meck:expect(pgapp, equery, fun(_Query, Params) -> querymock(Params) end), 25 | meck:new(ct_fetch, [passthrough]), 26 | meck:expect(ct_fetch, fetch_sth, fun() -> K end), 27 | noupdate = leaf_iterate:scheduled_check(), 28 | meck:unload(pgapp), 29 | meck:unload(ct_fetch), 30 | ok. 31 | 32 | update(_Config) -> 33 | K = "{\"tree_size\":10502586,\"timestamp\":1449817937400,\"sha256_root_hash\":\"qeBs0XUYqtWTMYTEbnIKQhQefv5eOCl+dZCFwPrpljk=\",\"tree_head_signature\":\"BAMARzBFAiEA5xzKR86R2jWkX67PBabhg1/v4GrfeeBEbK4bT4Npns0CIF9ew7he6hpMwbfsNDbZOnrzByo4EQcArov1jHQFBG0K\"}", 34 | meck:new(pgapp), 35 | meck:expect(pgapp, equery, fun(_Query, Params) -> querymock(Params) end), 36 | meck:new(ct_fetch, [passthrough]), 37 | meck:expect(ct_fetch, fetch_sth, fun() -> K end), 38 | [[{dNSName,"lolware.net"}, {dNSName,"www.lolware.net"}, 39 | {serial,"19F169D2A081E71A79CE2219220D0B582D6"}]] = 40 | leaf_iterate:scheduled_check(), 41 | meck:unload(pgapp), 42 | meck:unload(ct_fetch), 43 | ok. 44 | 45 | domain_check(_Config) -> 46 | meck:new(pgapp), 47 | meck:expect(pgapp, equery, fun(_Query, Params) -> querymock(Params) end), 48 | meck:new(ct_mail_alert), 49 | meck:expect(ct_mail_alert, send_alert, fun(Alerts, Domains, ID) -> 50 | [Alerts, Domains, ID] = [[{"lolware.net","technion@lolware.net"}, 51 | {"www.lolware.net","technion@lolware.net"}], 52 | [{dNSName,"lolware.net"},{dNSName,"www.lolware.net"}], 53 | {serial,"19F169D2A081E71A79CE2219220D0B582D6"}] end), 54 | domain_parse:cert_domain_list([[{dNSName,"lolware.net"}, 55 | {dNSName,"www.lolware.net"}, 56 | {serial,"19F169D2A081E71A79CE2219220D0B582D6"}]]), 57 | true = meck:validate(ct_mail_alert), 58 | meck:unload(pgapp), 59 | meck:unload(ct_mail_alert), 60 | ok. 61 | 62 | end_per_suite(_Config) -> 63 | ok. 64 | -------------------------------------------------------------------------------- /apps/ct_advisor/src/leaf_parse.erl: -------------------------------------------------------------------------------- 1 | -module(leaf_parse). 2 | -export([parse_leaf/1, xparse/1, get_subjects/1, get_serial/1, paddedhex/1]). 3 | 4 | -include_lib("public_key/include/public_key.hrl"). 5 | 6 | %% Extracts an encoded certificate from JSON 7 | -spec parse_leaf(_) -> any(). 8 | parse_leaf(RAW) -> 9 | {JSON} = jiffy:decode(RAW), 10 | proplists:get_value(<<"entries">>, JSON). 11 | 12 | %% Decodes the merkle leaf packed structure to return a certificate. 13 | -spec xparse(binary()) -> any(). 14 | xparse(MerkleLeafB64) -> 15 | MerkleLeafBin = base64:decode(MerkleLeafB64), 16 | % Logtype = 0. Crash on fail. 17 | <<_Version:8, _LeafType:8, _Timestamp:64, 0:16, 18 | _ASNLen:24, Cert/binary>> = MerkleLeafBin, 19 | public_key:pkix_decode_cert(Cert, otp). 20 | 21 | %% Parses the subjectnames from a certificate. 22 | -spec get_subjects(#'OTPCertificate'{tbsCertificate::#'OTPTBSCertificate'{extensions::[any()]}}) -> any(). 23 | get_subjects(Cert) -> 24 | try [Ext || Ext <- 25 | Cert#'OTPCertificate'.tbsCertificate#'OTPTBSCertificate'.extensions, 26 | Ext#'Extension'.extnID == {2, 5, 29, 17} ] of 27 | [] -> 28 | []; 29 | [FirstNameExtension| _] -> 30 | FirstNameExtension#'Extension'.extnValue 31 | catch % List will crash on certificates with asn1_NOVALUE 32 | error:function_clause -> 33 | [] 34 | end. 35 | 36 | -spec get_serial(#'OTPCertificate'{tbsCertificate::#'OTPTBSCertificate'{serialNumber::integer()}}) -> {'serial', string()}. 37 | get_serial(Cert) -> 38 | Serial = Cert#'OTPCertificate'.tbsCertificate#'OTPTBSCertificate' 39 | .serialNumber, 40 | {serial, paddedhex(Serial)}. 41 | 42 | % https://stackoverflow.com/questions/45821143/ssl-certificates-leading-zeros-are-displayed-in-windows-but-not-unix-ksh-shell 43 | -spec paddedhex(pos_integer()) -> string(). 44 | paddedhex(X) -> 45 | Hex = integer_to_list(X, 16), 46 | case hd(Hex) >= $8 of 47 | true -> 48 | "00" ++ Hex; 49 | _ -> 50 | Hex 51 | end. 52 | 53 | -ifdef(TEST). 54 | -include_lib("eunit/include/eunit.hrl"). 55 | -include("test_constants.hrl"). 56 | %All tests based upon ID 9742371 - lolware.net 57 | parse_leaf_test() -> 58 | [{LeafTest}] = parse_leaf(?TEST_LEAF_ENTRY), 59 | LeafTest2 = proplists:get_value(<<"leaf_input">>, LeafTest), 60 | ?assertEqual(?TEST_MTL, LeafTest2). 61 | 62 | paddedhex_test() -> 63 | ?assertEqual("00ED2DF6455F94C89589E12F4E2F174FA3", 64 | paddedhex(315265683328745785102777192423882182563)). 65 | 66 | get_serial_test() -> 67 | X509 = leaf_parse:xparse(?TEST_MTL), 68 | ?assertEqual({serial, "19F169D2A081E71A79CE2219220D0B582D6"}, 69 | get_serial(X509)). 70 | 71 | mtl_to_subjects_test() -> 72 | X509 = leaf_parse:xparse(?TEST_MTL), 73 | ?assertEqual(?TEST_DOMAIN_LIST, leaf_parse:get_subjects(X509)). 74 | 75 | -endif. 76 | 77 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ct_advisor 2 | ========== 3 | 4 | ct_advisor is a proactive alerting tool for [Google's Certificate Transparency](https://www.certificate-transparency.org/). 5 | 6 | # Shutdown Statement 7 | 8 | The production instance of CT Advisor is shutting down. Although this was open source, as far as I'm aware there were no other installations of this application. Accordingly, this repo is being archived. See below for further information. 9 | 10 | https://lolware.net/blog/shutdown-ctadvisor/ 11 | 12 | ## Original README 13 | 14 | It is running live on this [this link](https://ctadvisor.lolware.net) and we encourage you to register your domains there. 15 | 16 | Google offers a number of great options for an administrator to utilise this feature. Unfortunately being an early adopter, particularly if you run Windows servers or run SSL on appliances, makes it difficult to take advantage of this service. 17 | 18 | As an alternative option, this service continually polls the CT log, and will trigger alerts if a certificate is ever registered for your domain, by any CA in the CT program. This can be used to identify fraudulent certificates. 19 | 20 | This image this ct_advisor in action: 21 | 22 | ![CT Advisor Email](https://lolware.net/media/images/ct_advisor_email.jpg) 23 | 24 | 25 | Monitoring your domain 26 | ---------------------- 27 | 28 | This application has been running for some time at the following site: [ctadvisor.lolware.net](https://ctadvisor.lolware.net). 29 | 30 | Note that monitors are not instant. Some certificates have taken several days to show up in CT monitor logs. 31 | 32 | Setup 33 | ----- 34 | 35 | This application uses a PostgreSQL database, and an SMTP server. 36 | 37 | - Install the front end, ct_advisor_int 38 | - Create tables using the Rails frontend 39 | - Create priv/credentials.rr in the following format: 40 | 41 | ```erlang 42 | {database, {credentials, "localhost", "ct_advisor", "password"}}. 43 | {smtp, {credentials, "email-relay.com", "username", "password"}}. 44 | ``` 45 | 46 | Build 47 | ----- 48 | 49 | This application bundles the tested version of rebar3, and will pull its own external dependancies, of which there are several. Both eunit and Common Test suites are utilised. 50 | 51 | ```shell 52 | $ ./rebar3 xref 53 | $ ./rebar3 dialyzer 54 | $ ./rebar3 eunit 55 | $ ./rebar3 ct 56 | $ /.rebar3 release 57 | ``` 58 | 59 | In development 60 | -------------- 61 | It's far easier to utilise my instance of this tool than to attempt to run it yourself - I recommend doing so unless you wish to be involved in development. 62 | 63 | 64 | Contributing 65 | ------------ 66 | 67 | * In line with the above, potential contributors should be aware I am unlikely to merge and changes relating to features that I won't be using. 68 | * Code must produce no errors under dialyzer, xref or elvis 69 | * Complex functions must include eunit tests 70 | * Leave your politics at the door 71 | 72 | -------------------------------------------------------------------------------- /apps/ct_advisor/src/ct_mail_alert.erl: -------------------------------------------------------------------------------- 1 | -module(ct_mail_alert). 2 | -export([send_alert/3]). 3 | 4 | -record(credentials, {hostname, username, password}). 5 | 6 | %% Issues an email alert. 7 | 8 | -spec send_alert([{string(), string()}]|[], [tuple()], {serial, string()}) -> 9 | {'ok', pid()}|ok. 10 | send_alert([], _Certificate, {serial, _Serial}) -> 11 | ok; 12 | send_alert([{Domain, User}|Tail], Certificate, {serial, Serial}) -> 13 | lager:notice("We have an alert for ~p, ~p with cert ~p~n", 14 | [Domain, User, Certificate]), 15 | {ok, Config} = file:consult("priv/credentials.rr"), 16 | Creds = proplists:get_value(smtp, Config), 17 | {ok, Pid} = gen_smtp_client:send({"ctadvisor@lolware.net", [User], 18 | "Subject: SSL Has been issued for monitored domain\r\n" 19 | "From: ctadvisor@lolware.net\r\nTo: " ++ User ++ "\r\n\r\n" 20 | "ct_advisor has detected the issuance of an SSL certificate for domain " 21 | ++ Domain ++ " for which you are registered. If this was not you, you" 22 | " may wish to investigate. You can obtain further information " 23 | "by reviewing the issued certificate here: https://crt.sh/?serial=" 24 | ++ Serial ++ "\r\nIf you would like to unsubscribe from this service " 25 | "please visit this link: " 26 | "https://ctadvisor.lolware.net/registrations/unsubscribe"}, 27 | [{relay, Creds#credentials.hostname}, 28 | {username, Creds#credentials.username}, 29 | {password, Creds#credentials.password}, {port, 587} ]), 30 | send_alert(Tail, Certificate, {serial, Serial}), 31 | {ok, Pid}. 32 | 33 | -ifdef(TEST). 34 | -include_lib("eunit/include/eunit.hrl"). 35 | 36 | send_bouncemail_test() -> 37 | % AWS SES will still accept a bounce - but should generate an alert to SNS 38 | {T, Pid} = send_alert([{"lolwaretest.net", 39 | "bounce@simulator.amazonses.com"}], 40 | [{dNSName, "www.lolwaretest.net"}, {dNSName, "lolwaretest.net"}], 41 | {serial, "19F169D2A081E71A79CE2219220D0B582D6"}), 42 | ?assertEqual(ok, T), 43 | unlink(Pid), 44 | Monitor = erlang:monitor(process, Pid), 45 | Response = receive 46 | {'DOWN', Monitor, process, Pid, Error} -> 47 | Error 48 | after 5000 -> 49 | nomessage 50 | end, 51 | ?assertEqual(normal, Response). 52 | 53 | send_mail_test() -> 54 | {T, Pid} = send_alert([{"lolware.net", 55 | "success@simulator.amazonses.com"}, 56 | {"www.lolware.net", "success@simulator.amazonses.com"}], 57 | [{dNSName, "www.lolwaretest.net"}, {dNSName, "lolwaretest.net"}], 58 | {serial, "19F169D2A081E71A79CE2219220D0B582D6"}), 59 | ?assertEqual(ok, T), 60 | unlink(Pid), 61 | Monitor = erlang:monitor(process, Pid), 62 | Response = receive 63 | {'DOWN', Monitor, process, Pid, Error} -> 64 | Error 65 | after 5000 -> 66 | nomessage 67 | end, 68 | ?assertEqual(normal, Response). 69 | -endif. 70 | -------------------------------------------------------------------------------- /apps/ct_advisor/src/leaf_iterate.erl: -------------------------------------------------------------------------------- 1 | -module(leaf_iterate). 2 | -export([scheduled_check/0]). 3 | %enumberate_ids/2 is not a programatically required export. 4 | % It is often required for debugging however. 5 | -export([enumerate_ids/2]). 6 | 7 | %% The entry function. This checks the latest recorded certificate and fires 8 | %% processing based on that. 9 | -spec scheduled_check() -> 'noupdate' | [any(), ...]. 10 | scheduled_check() -> 11 | STH = ct_fetch:fetch_sth(), 12 | Latest = ct_fetch:parse_sth(STH), 13 | lookup_updates(Latest). 14 | 15 | % Compares the input STH with the last checked value based on database lookup. 16 | % Calls new checks as appropriate. 17 | -spec lookup_updates(pos_integer()) -> 'noupdate' | [any(), ...]. 18 | lookup_updates(Latest) -> 19 | {ok, _Columns, Rows} = pgapp:equery("SELECT latest FROM STH", []), 20 | case Rows of 21 | [{LastLookup}] when Latest > LastLookup -> 22 | lager:info("Performing checks: ~B~n", [Latest]), 23 | run_checks(LastLookup, Latest); 24 | _ -> 25 | lager:debug("No updates, latest still: ~B~n", [Latest]), 26 | noupdate 27 | end. 28 | 29 | %% Downloads and parses a a range of certificate id's, updates 30 | %% last checked value in database 31 | -spec run_checks(pos_integer(), pos_integer()) -> [any(), ...]. 32 | run_checks(LOW, HIGH) -> 33 | {FROM, TO} = get_range(LOW, HIGH), 34 | lager:info("Running between: ~B and ~B~n", [FROM, TO]), 35 | Domains = enumerate_ids(FROM, TO), 36 | % This task can involve delays, spawning here makes this work somewhat 37 | % asynchronous. 38 | spawn(domain_parse, cert_domain_list, [Domains]), 39 | {ok, 1} = pgapp:equery("UPDATE sth SET latest = $1", [TO + 1]), 40 | Domains. 41 | 42 | %% Rate limiting function - if a range is higher than a configured 43 | %% value - reduce the range. 44 | -define(ITERATIONS, 512). 45 | -spec get_range(pos_integer(), pos_integer()) -> {pos_integer(), pos_integer()}. 46 | get_range(LOW, HIGH) when HIGH > LOW -> 47 | % Note the highest lookup should be STH -1 48 | % We also rate limit lookups per run 49 | case (HIGH - LOW) of 50 | Diff when Diff > ?ITERATIONS -> 51 | {LOW, LOW+?ITERATIONS}; 52 | _Diff -> 53 | {LOW, HIGH-1} 54 | end. 55 | 56 | %% Will fetch a certificate and use the various parsing functions to 57 | %% extract a list of domains on that certificate. 58 | -spec get_domain_from_id(pos_integer()) -> any(). 59 | get_domain_from_id(MTL) -> 60 | try leaf_parse:xparse(MTL) of 61 | X509 -> 62 | leaf_parse:get_subjects(X509) ++ [leaf_parse:get_serial(X509)] 63 | catch 64 | _:_ -> 65 | [] 66 | end. 67 | 68 | -spec enumerate_ids(pos_integer(), pos_integer()) -> [any(), ...]. 69 | enumerate_ids(FROM, TO) -> 70 | Entries = ct_fetch:fetch_entry(FROM, TO), 71 | Leaves = leaf_parse:parse_leaf(Entries), 72 | MTLs = [proplists:get_value(<<"leaf_input">>, MTLs) || {MTLs} <- Leaves ], 73 | [get_domain_from_id(MTL) || MTL <- MTLs]. 74 | 75 | -ifdef(TEST). 76 | -include_lib("eunit/include/eunit.hrl"). 77 | -include("test_constants.hrl"). 78 | 79 | lookup_fixture_test_() -> 80 | {setup, fun connect/0, fun teardown/1, fun lookup/0}. 81 | 82 | connect() -> 83 | application:ensure_all_started(pgapp), 84 | db_connect:db_connect(). 85 | 86 | teardown(_C) -> 87 | application:stop(pgapp), 88 | ok. 89 | 90 | ranges_test() -> 91 | ?assertEqual({7, 7}, get_range(7, 8)), 92 | ?assertEqual({7, 519}, get_range(7, 540)). 93 | 94 | lookup() -> 95 | ?assertEqual(noupdate, lookup_updates(1025)). 96 | 97 | enumerate_test() -> 98 | ?assertEqual(?TEST_ENUMERATED_DOMAINS, enumerate_ids(10502585, 10502585)). 99 | 100 | -endif. 101 | -------------------------------------------------------------------------------- /apps/ct_advisor/include/test_constants.hrl: -------------------------------------------------------------------------------- 1 | % Several EUnit tests involve very long strings. Define them here to keep 2 | % main source clean. 3 | -define(TEST_LOOKUP_DOMAINS, [{dNSName,"lolware.net"},{dNSName,"www.lolware.net"}]). 4 | -define(TEST_NONDNS_DOMAINS, [{dNSName,"irrelevant.com"}, {rfc822Name,"email@lolware.net"}]). 5 | -define(TEST_ENUMERATED_DOMAINS, [[{dNSName,"lolware.net"},{dNSName,"www.lolware.net"},{serial,"19F169D2A081E71A79CE2219220D0B582D6"}]]). 6 | -define(TEST_STH, "{\"tree_size\":9910235,\"timestamp\":1448090100891,\"sha256_root_hash\":\"f/yL52udFFrqHzwxLZwpWKbBV1PxlqlanZ7dTT+3Ylo=\",\"tree_head_signature\":\"BAMARzBFAiEAiBAlcFyk8a0wz5KdWugGbZL+DZ8gXq7gKDoiu+eDbcICIFrNiPLM/oHPDTge3B7XsXmiYf/kaCp96+BbFt30sj4o\"}"). 7 | -define(TEST_DOMAIN_LIST, [{dNSName,"lolware.net"},{dNSName,"www.lolware.net"}]). 8 | -define(TEST_MTL, <<"AAAAAAFQ6ROYSAAAAAUSMIIFDjCCA/agAwIBAgISAZ8WnSoIHnGnnOIhkiDQtYLWMA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTAeFw0xNTExMDgyMDUwMDBaFw0xNjAyMDYyMDUwMDBaMBYxFDASBgNVBAMTC2xvbHdhcmUubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1SePrSId4PdCXLuB3PH+6nMaQ5yDEUsCRPJ6oJkR/uqeOTgmHdIelpI3FAtHvc/BqxGe8phwkEtnAFtnYTtKSPmwJS6LF/kNWEjvUd1k6PXQMfjgmRT4XJcnOfAU/dGlZlc6hLxL/H40Ol7ohX4oepJBsakkFBx+gZ07ufh91U2DKL/37zghuZ4P61S5AW4rojvYsIE7B3jJAYhqoZO82l/ywDF/0RYdJmEoiLRZULSiVqJTdLSi6+6qNjsiEz9dtP2xj1/kCM7cLXoLLfnCHhX76HSzpzl5Hs4AwovpQ15yeo7WE8RkBP7l5bndYNXtxLATPXNmhfaGEL/vb87NewIDAQABo4ICIDCCAhwwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSisGjWvbZ/u0XSHaj7LoebOAAMcDAfBgNVHSMEGDAWgBSoSmpjBH3duubRObemRWXv86jsoTBwBggrBgEFBQcBAQRkMGIwLwYIKwYBBQUHMAGGI2h0dHA6Ly9vY3NwLmludC14MS5sZXRzZW5jcnlwdC5vcmcvMC8GCCsGAQUFBzAChiNodHRwOi8vY2VydC5pbnQteDEubGV0c2VuY3J5cHQub3JnLzAnBgNVHREEIDAeggtsb2x3YXJlLm5ldIIPd3d3LmxvbHdhcmUubmV0MIIBAAYDVR0gBIH4MIH1MAoGBmeBDAECATAAMIHmBgsrBgEEAYLfEwEBATCB1jAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwgasGCCsGAQUFBwICMIGeDIGbVGhpcyBDZXJ0aWZpY2F0ZSBtYXkgb25seSBiZSByZWxpZWQgdXBvbiBieSBSZWx5aW5nIFBhcnRpZXMgYW5kIG9ubHkgaW4gYWNjb3JkYW5jZSB3aXRoIHRoZSBDZXJ0aWZpY2F0ZSBQb2xpY3kgZm91bmQgYXQgaHR0cHM6Ly9sZXRzZW5jcnlwdC5vcmcvcmVwb3NpdG9yeS8wDQYJKoZIhvcNAQELBQADggEBAFudN5CzroT/YpaY7VVutGi85lbYWdYu9n3FDrmA8t10NvR4S78PftF87pxHQYRd7oEWAnOtxTt0y3oSCzkWzONTp1VzU7TBd2orHQ7jDn4XQ+Zbm9s1nO3/Ykzk+nQAMbSiBtAaxCwExT/UAb2fuKyIHkh+CXyx+eVfaz7MEXRmLuhsmn6PIrQu9FpaPJ+QAn8uD/LBA6IFBCcvVwnO9sOpnh+vJTQj6jpyY7jjPWBgFVLAXflS1Y1n7tKYZf4drKoDhZ9cV+5bOS/mPFZYnHMotDf9XAZSQjbJ6h+aF3GydYRQqR5SKTJp+MvExTUlSiS5+Q2IzGfzjo+gD2WKP7kAAA==">>). 9 | -define(TEST_LEAF_ENTRY, "{\"entries\":[{\"leaf_input\":\"AAAAAAFQ6ROYSAAAAAUSMIIFDjCCA/agAwIBAgISAZ8WnSoIHnGnnOIhkiDQtYLWMA0GCSqGSIb3DQEBCwUAMEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQDExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMTAeFw0xNTExMDgyMDUwMDBaFw0xNjAyMDYyMDUwMDBaMBYxFDASBgNVBAMTC2xvbHdhcmUubmV0MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1SePrSId4PdCXLuB3PH+6nMaQ5yDEUsCRPJ6oJkR/uqeOTgmHdIelpI3FAtHvc/BqxGe8phwkEtnAFtnYTtKSPmwJS6LF/kNWEjvUd1k6PXQMfjgmRT4XJcnOfAU/dGlZlc6hLxL/H40Ol7ohX4oepJBsakkFBx+gZ07ufh91U2DKL/37zghuZ4P61S5AW4rojvYsIE7B3jJAYhqoZO82l/ywDF/0RYdJmEoiLRZULSiVqJTdLSi6+6qNjsiEz9dtP2xj1/kCM7cLXoLLfnCHhX76HSzpzl5Hs4AwovpQ15yeo7WE8RkBP7l5bndYNXtxLATPXNmhfaGEL/vb87NewIDAQABo4ICIDCCAhwwDgYDVR0PAQH/BAQDAgWgMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAMBgNVHRMBAf8EAjAAMB0GA1UdDgQWBBSisGjWvbZ/u0XSHaj7LoebOAAMcDAfBgNVHSMEGDAWgBSoSmpjBH3duubRObemRWXv86jsoTBwBggrBgEFBQcBAQRkMGIwLwYIKwYBBQUHMAGGI2h0dHA6Ly9vY3NwLmludC14MS5sZXRzZW5jcnlwdC5vcmcvMC8GCCsGAQUFBzAChiNodHRwOi8vY2VydC5pbnQteDEubGV0c2VuY3J5cHQub3JnLzAnBgNVHREEIDAeggtsb2x3YXJlLm5ldIIPd3d3LmxvbHdhcmUubmV0MIIBAAYDVR0gBIH4MIH1MAoGBmeBDAECATAAMIHmBgsrBgEEAYLfEwEBATCB1jAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5vcmcwgasGCCsGAQUFBwICMIGeDIGbVGhpcyBDZXJ0aWZpY2F0ZSBtYXkgb25seSBiZSByZWxpZWQgdXBvbiBieSBSZWx5aW5nIFBhcnRpZXMgYW5kIG9ubHkgaW4gYWNjb3JkYW5jZSB3aXRoIHRoZSBDZXJ0aWZpY2F0ZSBQb2xpY3kgZm91bmQgYXQgaHR0cHM6Ly9sZXRzZW5jcnlwdC5vcmcvcmVwb3NpdG9yeS8wDQYJKoZIhvcNAQELBQADggEBAFudN5CzroT/YpaY7VVutGi85lbYWdYu9n3FDrmA8t10NvR4S78PftF87pxHQYRd7oEWAnOtxTt0y3oSCzkWzONTp1VzU7TBd2orHQ7jDn4XQ+Zbm9s1nO3/Ykzk+nQAMbSiBtAaxCwExT/UAb2fuKyIHkh+CXyx+eVfaz7MEXRmLuhsmn6PIrQu9FpaPJ+QAn8uD/LBA6IFBCcvVwnO9sOpnh+vJTQj6jpyY7jjPWBgFVLAXflS1Y1n7tKYZf4drKoDhZ9cV+5bOS/mPFZYnHMotDf9XAZSQjbJ6h+aF3GydYRQqR5SKTJp+MvExTUlSiS5+Q2IzGfzjo+gD2WKP7kAAA==\",\"extra_data\":\"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\"}]}"). 10 | --------------------------------------------------------------------------------