diff --git a/lib/java_buildpack/framework/contrast_security_agent.rb b/lib/java_buildpack/framework/contrast_security_agent.rb index 9d5d3fb63c..99cb979b16 100644 --- a/lib/java_buildpack/framework/contrast_security_agent.rb +++ b/lib/java_buildpack/framework/contrast_security_agent.rb @@ -28,23 +28,29 @@ module Framework class ContrastSecurityAgent < JavaBuildpack::Component::VersionedDependencyComponent include JavaBuildpack::Util + def initialize(context) + super(context) + @logger = JavaBuildpack::Logging::LoggerFactory.instance.get_logger ContrastSecurityAgent + end + # (see JavaBuildpack::Component::BaseComponent#compile) def compile download_jar @droplet.copy_resources - - write_configuration @application.services.find_service(FILTER, API_KEY, SERVICE_KEY, TEAMSERVER_URL, - USERNAME)['credentials'] end # (see JavaBuildpack::Component::BaseComponent#release) def release - @droplet.java_opts.add_system_property('contrast.override.appname', application_name) unless appname_exist? + # Fetch the credentials and settings + credentials = @application.services.find_service(FILTER, API_KEY, SERVICE_KEY, TEAMSERVER_URL, + USERNAME)['credentials'] + + # Add the Contrast config via env vars + add_config_to_env credentials + # Add the -javaagent option to cause the agent to start with the JVM @droplet.java_opts - .add_system_property('contrast.dir', '$TMPDIR') - .add_preformatted_options("-javaagent:#{qualify_path(@droplet.sandbox + jar_name, @droplet.root)}=" \ - "#{qualify_path(contrast_config, @droplet.root)}") + .add_preformatted_options("-javaagent:#{qualify_path(@droplet.sandbox + jar_name, @droplet.root)}") end protected @@ -78,40 +84,14 @@ def supports? private_constant :API_KEY, :FILTER, :INFLECTION_VERSION, :PLUGIN_PACKAGE, :SERVICE_KEY, :TEAMSERVER_URL, :USERNAME - def add_contrast(doc, credentials) - contrast = doc.add_element('contrast') - (contrast.add_element 'id').add_text('default') - (contrast.add_element 'global-key').add_text(credentials[API_KEY]) - (contrast.add_element 'url').add_text("#{credentials[TEAMSERVER_URL]}/Contrast/s/") - (contrast.add_element 'results-mode').add_text('never') - - add_user contrast, credentials - add_plugins contrast - end - - def add_plugins(contrast) - plugin_group = contrast.add_element('plugins') - - (plugin_group.add_element 'plugin').add_text("#{PLUGIN_PACKAGE}.security.SecurityPlugin") - (plugin_group.add_element 'plugin').add_text("#{PLUGIN_PACKAGE}.architecture.ArchitecturePlugin") - (plugin_group.add_element 'plugin').add_text("#{PLUGIN_PACKAGE}.appupdater.ApplicationUpdatePlugin") - (plugin_group.add_element 'plugin').add_text("#{PLUGIN_PACKAGE}.sitemap.SitemapPlugin") - (plugin_group.add_element 'plugin').add_text("#{PLUGIN_PACKAGE}.frameworks.FrameworkSupportPlugin") - (plugin_group.add_element 'plugin').add_text("#{PLUGIN_PACKAGE}.http.HttpPlugin") - end - - def add_user(contrast, credentials) - user = contrast.add_element('user') - (user.add_element 'id').add_text(credentials[USERNAME]) - (user.add_element 'key').add_text(credentials[SERVICE_KEY]) - end - def application_name @application.details['application_name'] || 'ROOT' end def appname_exist? - @droplet.java_opts.any? { |java_opt| java_opt =~ /contrast.override.appname/ } + @droplet.java_opts.any? do |java_opt| + java_opt =~ /contrast\.override\.appname/ || java_opt =~ /contrast\.application\.name/ + end end def contrast_config @@ -122,16 +102,62 @@ def short_version "#{@version[0]}.#{@version[1]}.#{@version[2]}" end - def write_configuration(credentials) - doc = REXML::Document.new + # Add Contrast config to the env variables of the droplet. + def add_config_to_env(credentials) + env_vars = @droplet.environment_variables + + # Add any extra environment variables that start with CONTRAST__ + process_extra_env_vars credentials, env_vars + + # Add the config in the backwards compatible old format setting name + add_env_var env_vars, 'CONTRAST__API__API_KEY', credentials[API_KEY] + add_env_var env_vars, 'CONTRAST__API__SERVICE_KEY', credentials[SERVICE_KEY] + add_env_var env_vars, 'CONTRAST__API__URL', "#{credentials[TEAMSERVER_URL]}/Contrast" + add_env_var env_vars, 'CONTRAST__API__USER_NAME', credentials[USERNAME] - add_contrast doc, credentials + add_env_var env_vars, 'CONTRAST__AGENT__CONTRAST_WORKING_DIR', '$TMPDIR' - contrast_config.open(File::CREAT | File::WRONLY) { |f| f.write(doc) } + app_name = application_name + add_env_var env_vars, 'CONTRAST__APPLICATION__NAME', app_name unless appname_exist? + + # Add the config for the proxy, if it exists + add_proxy_config credentials, env_vars + end + + # Add any generic new config from the broker, for any entry that starts with CONTRAST__ add to the env + # The intention is to allow the broker to add any new config that it wants to, without needing to modify the + # buildpack + def process_extra_env_vars(credentials, env_vars) + credentials.each do |key, value| + # Add any that start with CONTRAST__ AND non-empty values + matched = key.match?(/^CONTRAST__/) && !value.to_s.empty? + add_env_var env_vars, key, value if matched + end + end + + def add_env_var(env_vars, key, value) + env_vars.add_environment_variable key, value + end + + def add_proxy_config(credentials, env_vars) + host_set = credentials_value_set?(credentials, 'proxy_host') + add_env_var env_vars, 'CONTRAST__API__PROXY__HOST', credentials['proxy_host'] if host_set + + port_set = credentials_value_set?(credentials, 'proxy_port') + add_env_var env_vars, 'CONTRAST__API__PROXY__PORT', credentials['proxy_port'] if port_set + + pass_set = credentials_value_set?(credentials, 'proxy_pass') + add_env_var env_vars, 'CONTRAST__API__PROXY__PASS', credentials['proxy_pass'] if pass_set + + user_set = credentials_value_set?(credentials, 'proxy_user') + add_env_var env_vars, 'CONTRAST__API__PROXY__USER', credentials['proxy_user'] if user_set + end + + def credentials_value_set?(credentials, key) + !credentials[key].to_s.empty? end end end - end diff --git a/spec/java_buildpack/framework/contrast_security_agent_spec.rb b/spec/java_buildpack/framework/contrast_security_agent_spec.rb index 9ffb86cb90..623aa7ff94 100644 --- a/spec/java_buildpack/framework/contrast_security_agent_spec.rb +++ b/spec/java_buildpack/framework/contrast_security_agent_spec.rb @@ -32,10 +32,19 @@ before do allow(services).to receive(:one_service?).with(/contrast-security/, 'api_key', 'service_key', 'teamserver_url', 'username').and_return(true) - allow(services).to receive(:find_service).and_return('credentials' => { 'teamserver_url' => 'a_url', + allow(services).to receive(:find_service).and_return('credentials' => { 'teamserver_url' => 'https://host.com', 'username' => 'contrast_user', - 'api_key' => 'api_test', - 'service_key' => 'service_test' }) + 'api_key' => 'api_key_test', + 'service_key' => 'service_key_test', + 'proxy_host' => 'proxy_host_test', + 'proxy_port' => 8080, + 'proxy_user' => 'proxy_user_test', + 'proxy_pass' => 'proxy_password_test', + 'CONTRAST__INVENTORY__LIBRARY_DIRS' => + '/lib/dir', + 'CONTRAST__API__TIMEOUT_MS' => '30000', + 'CONTRAST__API__URL' => + 'invalid_override_url' }) end it 'detects with contrastsecurity service' do @@ -70,20 +79,10 @@ expect(java_opts.to_s).to include('java-agent-3.4.3.jar') end - it 'updates JAVA_OPTS' do + it 'updates JAVA_OPTS to enable the agent' do component.release - expect(java_opts).to include('-javaagent:$PWD/.java-buildpack/contrast_security_agent/contrast-engine-0.0.0.jar' \ - '=$PWD/.java-buildpack/contrast_security_agent/contrast.config') - expect(java_opts).to include('-Dcontrast.dir=$TMPDIR') - expect(java_opts).to include('-Dcontrast.override.appname=test-application-name') - end - - it 'created contrast.config', - cache_fixture: 'stub-contrast-security-agent.jar' do - - component.compile - expect(sandbox + 'contrast.config').to exist + expect(java_opts).to include('-javaagent:$PWD/.java-buildpack/contrast_security_agent/contrast-engine-0.0.0.jar') end it 'does not override app name if there is an existing appname' do @@ -92,7 +91,113 @@ component.release expect(java_opts).to include('-Dcontrast.override.appname=NAME_ALREADY_OVERRIDDEN') - expect(java_opts).not_to include('-Dcontrast.override.appname=test-application-name') + expect(environment_variables).not_to include('CONTRAST__APPLICATION__NAME=test-application-name') + end + + it 'does not override app name if there is an existing name' do + java_opts.add_system_property('contrast.application.name', 'NAME_ALREADY_OVERRIDDEN') + + component.release + + expect(java_opts).to include('-Dcontrast.application.name=NAME_ALREADY_OVERRIDDEN') + expect(environment_variables).not_to include('CONTRAST__APPLICATION__NAME=test-application-name') + end + + it 'sets in env vars the credentials for connecting to Contrast UI' do + component.release + + expect(environment_variables).to include('CONTRAST__API__URL=https://host.com/Contrast') + expect(environment_variables).to include('CONTRAST__API__API_KEY=api_key_test') + expect(environment_variables).to include('CONTRAST__API__SERVICE_KEY=service_key_test') + expect(environment_variables).to include('CONTRAST__API__USER_NAME=contrast_user') + end + + it 'sets in env vars the working directory for Contrast' do + component.release + + expect(environment_variables).to include('CONTRAST__AGENT__CONTRAST_WORKING_DIR=$TMPDIR') + end + + it 'sets in env vars the proxy settings when using a proxy' do + component.release + + expect(environment_variables).to include('CONTRAST__API__PROXY__HOST=proxy_host_test') + expect(environment_variables).to include('CONTRAST__API__PROXY__PORT=8080') + expect(environment_variables).to include('CONTRAST__API__PROXY__USER=proxy_user_test') + expect(environment_variables).to include('CONTRAST__API__PROXY__PASS=proxy_password_test') + end + + it 'sets in env vars any other CONTRAST_ settings that exist' do + component.release + + # Sets them without knowing what they are ahead of time + expect(environment_variables).to include('CONTRAST__INVENTORY__LIBRARY_DIRS=/lib/dir') + expect(environment_variables).to include('CONTRAST__API__TIMEOUT_MS=30000') + end + + it 'specifically named settings override any generic CONTRAST__ settings' do + component.release + + # The standard property `teamserver_url` was set along with CONTRAST__API__URL, so the former must be used + expect(environment_variables).to include('CONTRAST__API__URL=https://host.com/Contrast') + end + + end + + # Test with different settings from the service broker + context do + + before do + allow(services).to receive(:one_service?).with(/contrast-security/, 'api_key', 'service_key', 'teamserver_url', + 'username').and_return(true) + allow(services).to receive(:find_service).and_return('credentials' => { 'teamserver_url' => 'https://host.com', + 'username' => 'contrast_user', + 'api_key' => 'api_key_test', + 'service_key' => 'service_key_test' }) + end + + it 'proxy settings not applied to env vars when not set by broker' do + component.release + + # convert to string to search for the env var by name + env_var_str = environment_variables.to_s + + expect(env_var_str).not_to include('CONTRAST__API__PROXY__HOST') + expect(env_var_str).not_to include('CONTRAST__API__PROXY__PORT') + expect(env_var_str).not_to include('CONTRAST__API__PROXY__USER') + expect(env_var_str).not_to include('CONTRAST__API__PROXY__PASS') + end + + end + + # Test with null and empty values for the proxy settings + context do + + before do + allow(services).to receive(:one_service?).with(/contrast-security/, 'api_key', 'service_key', 'teamserver_url', + 'username').and_return(true) + allow(services).to receive(:find_service).and_return('credentials' => { 'teamserver_url' => 'https://host.com', + 'username' => 'contrast_user', + 'api_key' => 'api_key_test', + 'service_key' => 'service_key_test', + # Test nil and empty values + 'proxy_host' => nil, + 'CONTRAST__IGNORE_01' => '', + 'CONTRAST__IGNORE_02' => nil }) + end + + it 'proxy settings handle nil and empty' do + component.release + + # convert to string to search for the env var by name + env_var_str = environment_variables.to_s + + expect(env_var_str).not_to include('CONTRAST__API__PROXY__HOST') + expect(env_var_str).not_to include('CONTRAST__API__PROXY__PORT') + expect(env_var_str).not_to include('CONTRAST__API__PROXY__USER') + expect(env_var_str).not_to include('CONTRAST__API__PROXY__PASS') + expect(env_var_str).not_to include('CONTRAST__IGNORE_01') + expect(env_var_str).not_to include('CONTRAST__IGNORE_02') end end