All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.expedia.www.haystack.commons.secretDetector.span.SpanSecretMasker Maven / Gradle / Ivy

/*
 * Copyright 2018 Expedia, Inc.
 *
 *       Licensed under the Apache License, Version 2.0 (the "License");
 *       you may not use this file except in compliance with the License.
 *       You may obtain a copy of the License at
 *
 *           http://www.apache.org/licenses/LICENSE-2.0
 *
 *       Unless required by applicable law or agreed to in writing, software
 *       distributed under the License is distributed on an "AS IS" BASIS,
 *       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *       See the License for the specific language governing permissions and
 *       limitations under the License.
 *
 */
package com.expedia.www.haystack.commons.secretDetector.span;

import com.expedia.open.tracing.Log;
import com.expedia.open.tracing.Span;
import com.expedia.open.tracing.Tag;
import com.expedia.www.haystack.commons.secretDetector.DetectorBase;
import com.expedia.www.haystack.commons.secretDetector.FinderNameAndServiceName;
import com.expedia.www.haystack.commons.secretDetector.HaystackFinderEngine;
import com.expedia.www.haystack.metrics.MetricObjects;
import com.google.common.base.Strings;
import com.google.protobuf.ByteString;
import com.netflix.servo.monitor.Counter;
import com.netflix.servo.util.VisibleForTesting;
import org.apache.kafka.streams.kstream.ValueMapper;
import org.slf4j.LoggerFactory;

import java.time.Clock;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static com.expedia.www.haystack.commons.config.Configuration.WHITELIST_S3_ITEM_NAME;
import static com.expedia.www.haystack.commons.secretDetector.span.SpanDetector.ERRORS_METRIC_GROUP;

/**
 * Finds that tag keys and field keys in a Span that contain secrets.
 */
@SuppressWarnings("WeakerAccess")
public class SpanSecretMasker extends DetectorBase implements ValueMapper {
    @VisibleForTesting
    static final Map COUNTERS = Collections.synchronizedMap(new HashMap<>());
    @VisibleForTesting
    static final String MASKED_BY_HAYSTACK = "Confidential Data Masked By Haystack";
    @VisibleForTesting
    static final byte[] MASKED_BY_HAYSTACK_BYTES = MASKED_BY_HAYSTACK.getBytes();

    private static final String COUNTER_NAME = "SECRETS";
    private static final Map> CACHED_FINDER_NAME_AND_SECRET_NAME_OBJECTS =
            new ConcurrentHashMap<>();
    private static final ByteString MASKED_BY_HAYSTACK_BYTE_STRING = ByteString.copyFrom(MASKED_BY_HAYSTACK_BYTES);
    private final Factory factory;
    private final String application;
    private final SpanNameAndCountRecorder spanNameAndCountRecorder;

    public SpanSecretMasker(String bucket, String subsystem, String application) {
        //noinspection LoggerInitializedWithForeignClass
        this(new HaystackFinderEngine(new MetricObjects(), subsystem, application),
                new Factory(),
                new SpanS3ConfigFetcher(bucket, WHITELIST_S3_ITEM_NAME),
                new SpanNameAndCountRecorder(LoggerFactory.getLogger(SpanNameAndCountRecorder.class), Clock.systemUTC()),
                application);
    }

    public SpanSecretMasker(HaystackFinderEngine haystackFinderEngine,
                            SpanSecretMasker.Factory spanSecretMaskerFactory,
                            SpanS3ConfigFetcher spanS3ConfigFetcher,
                            SpanNameAndCountRecorder spanNameAndCountRecorder,
                            String application) {
        super(haystackFinderEngine, spanS3ConfigFetcher);
        this.factory = spanSecretMaskerFactory;
        this.spanNameAndCountRecorder = spanNameAndCountRecorder;
        this.application = application;
    }

    private static class BuildersForTags {
        Span.Builder spanBuilder;
        Tag.Builder tagBuilder;
    }

    private Span maskSecretsInTags(Span span) {
        final List tags = span.getTagsList();
        BuildersForTags buildersForTags = null;
        for (int tagIndex = 0; tagIndex < tags.size(); tagIndex++) {
            final Tag tag = tags.get(tagIndex);
            if (!Strings.isNullOrEmpty(tag.getVStr())) {
                final Map> mapOfTypeToKeysOfSecrets = findSecrets(tag, tag.getVStr());
                if (isNonWhitelistedSecretFound(span, mapOfTypeToKeysOfSecrets)) {
                    buildersForTags = prepareForBuild(span, tag, buildersForTags);
                    buildersForTags.tagBuilder.setVStr(MASKED_BY_HAYSTACK);
                    buildIntoTags(buildersForTags, tagIndex);
                }
            } else if (!tag.getVBytes().isEmpty()) {
                @SuppressWarnings("ObjectAllocationInLoop")
                final String input = new String(tag.getVBytes().toByteArray());
                final Map> mapOfTypeToKeysOfSecrets = findSecrets(tag, input);
                if (isNonWhitelistedSecretFound(span, mapOfTypeToKeysOfSecrets)) {
                    buildersForTags = prepareForBuild(span, tag, buildersForTags);
                    buildersForTags.tagBuilder.setVBytes(MASKED_BY_HAYSTACK_BYTE_STRING);
                    buildIntoTags(buildersForTags, tagIndex);
                }
            }
        }
        return (buildersForTags == null) ? span : buildersForTags.spanBuilder.build();
    }

    private static void buildIntoTags(BuildersForTags buildersForTags, int tagIndex) {
        buildersForTags.spanBuilder.setTags(tagIndex, buildersForTags.tagBuilder.build());
    }

    private static BuildersForTags prepareForBuild(Span span,
                                                   Tag tag,
                                                   BuildersForTags buildersForTags) {
        final BuildersForTags buildersForTagsToUseForBuildCalls;
        if(buildersForTags == null) {
            buildersForTagsToUseForBuildCalls = new BuildersForTags();
        } else {
            buildersForTagsToUseForBuildCalls = buildersForTags;
        }
        if (buildersForTagsToUseForBuildCalls.spanBuilder == null) {
            buildersForTagsToUseForBuildCalls.spanBuilder = Span.newBuilder();
            buildersForTagsToUseForBuildCalls.spanBuilder.mergeFrom(span);
        }
        buildersForTagsToUseForBuildCalls.tagBuilder = mergeTagIntoNewTagBuilder(tag);
        return buildersForTagsToUseForBuildCalls;
    }

    private static class BuildersForLogFields extends BuildersForTags {
        Log.Builder logBuilder;
    }

    @SuppressWarnings("MethodWithMultipleLoops")
    private Span maskSecretsInLogFields(Span span) {
        BuildersForLogFields buildersForLogFields = null;
        for (int logIndex = 0; logIndex < span.getLogsList().size(); logIndex++) {
            final Log log = span.getLogs(logIndex);
            final List tags = log.getFieldsList();
            for (int tagIndex = 0; tagIndex < tags.size(); tagIndex++) {
                final Tag tag = tags.get(tagIndex);
                if (!Strings.isNullOrEmpty(tag.getVStr())) {
                    final Map> mapOfTypeToKeysOfSecrets = findSecrets(tag, tag.getVStr());
                    if (isNonWhitelistedSecretFound(span, mapOfTypeToKeysOfSecrets)) {
                        buildersForLogFields = prepareForBuild(span, tag, log, buildersForLogFields);
                        buildersForLogFields.tagBuilder.setVStr(MASKED_BY_HAYSTACK);
                        buildIntoLogFields(buildersForLogFields, logIndex, tagIndex);
                    }
                } else if (!tag.getVBytes().isEmpty()) {
                    @SuppressWarnings("ObjectAllocationInLoop")
                    final String input = new String(tag.getVBytes().toByteArray());
                    final Map> mapOfTypeToKeysOfSecrets = findSecrets(tag, input);
                    if (isNonWhitelistedSecretFound(span, mapOfTypeToKeysOfSecrets)) {
                        buildersForLogFields = prepareForBuild(span, tag, log, buildersForLogFields);
                        buildersForLogFields.tagBuilder.setVBytes(MASKED_BY_HAYSTACK_BYTE_STRING);
                        buildIntoLogFields(buildersForLogFields, logIndex, tagIndex);
                    }
                }
            }
        }
        return (buildersForLogFields == null) ? span : buildersForLogFields.spanBuilder.build();
    }

    private static void buildIntoLogFields(BuildersForLogFields buildersForLogFields, int logIndex, int tagIndex) {
        buildersForLogFields.logBuilder.setFields(tagIndex, buildersForLogFields.tagBuilder.build());
        buildersForLogFields.spanBuilder.setLogs(logIndex, buildersForLogFields.logBuilder.build());
    }

    private static BuildersForLogFields prepareForBuild(Span span,
                                                        Tag tag,
                                                        Log log,
                                                        BuildersForLogFields buildersForLogFields) {
        final BuildersForLogFields buildersForLogFieldsToUseForBuildCalls =
                createBuildersForLogFieldsIfNull(buildersForLogFields);
        if (buildersForLogFieldsToUseForBuildCalls.spanBuilder == null) {
            buildersForLogFieldsToUseForBuildCalls.spanBuilder = Span.newBuilder();
            buildersForLogFieldsToUseForBuildCalls.spanBuilder.mergeFrom(span);
        }
        buildersForLogFieldsToUseForBuildCalls.logBuilder = mergeLogIntoNewLogBuilder(log);
        buildersForLogFieldsToUseForBuildCalls.tagBuilder = mergeTagIntoNewTagBuilder(tag);
        return buildersForLogFieldsToUseForBuildCalls;
    }

    private static BuildersForLogFields createBuildersForLogFieldsIfNull(BuildersForLogFields buildersForLogFields) {
        final BuildersForLogFields buildersForLogFieldsToUseForBuildCalls;
        if(buildersForLogFields == null) {
            buildersForLogFieldsToUseForBuildCalls = new BuildersForLogFields();
        } else {
            buildersForLogFieldsToUseForBuildCalls = buildersForLogFields;
        }
        return buildersForLogFieldsToUseForBuildCalls;
    }

    private Map> findSecrets(Tag tag, String input) {
        final Map> mapOfTypeToValuesOfSecrets = haystackFinderEngine.findWithType(input);
        for (Map.Entry> stringListEntry : mapOfTypeToValuesOfSecrets.entrySet()) {
            stringListEntry.getValue().replaceAll(s -> tag.getKey());
        }
        return mapOfTypeToValuesOfSecrets; // but the tag values have now been replaced by the tag key
    }

    private boolean isNonWhitelistedSecretFound(Span span, Map> mapOfTypeToKeysOfSecrets) {
        return !mapOfTypeToKeysOfSecrets.isEmpty() && !areAllSecretsWhitelisted(span, mapOfTypeToKeysOfSecrets);
    }

    private static Log.Builder mergeLogIntoNewLogBuilder(Log log) {
        final Log.Builder newLogBuilder = Log.newBuilder();
        newLogBuilder.mergeFrom(log);
        return newLogBuilder;
    }

    private static Tag.Builder mergeTagIntoNewTagBuilder(Tag tag) {
        Tag.Builder maskedTagBuilder = Tag.newBuilder();
        maskedTagBuilder.mergeFrom(tag);
        return maskedTagBuilder;
    }

    @SuppressWarnings({"BooleanMethodIsAlwaysInverted", "MethodWithMultipleLoops"})
    private boolean areAllSecretsWhitelisted(Span span, Map> mapOfTypeToKeysOfSecrets) {
        final Iterator>> firstLevelIterator =
                mapOfTypeToKeysOfSecrets.entrySet().iterator();
        final String serviceName = span.getServiceName();
        final String operationName = span.getOperationName();
        while (firstLevelIterator.hasNext()) {
            final Map.Entry> finderNameToKeysOfSecrets = firstLevelIterator.next();
            final String finderName = finderNameToKeysOfSecrets.getKey();
            finderNameToKeysOfSecrets.getValue().removeIf(
                    tagName -> s3ConfigFetcher.isInWhiteList(finderName, serviceName, operationName, tagName));
            if (finderNameToKeysOfSecrets.getValue().isEmpty()) {
                firstLevelIterator.remove();
            } else {
                for (String tagName : finderNameToKeysOfSecrets.getValue()) {
                    spanNameAndCountRecorder.add(finderName, serviceName, operationName, tagName);
                }
                incrementCounter(serviceName, finderName, application);
            }
        }
        return mapOfTypeToKeysOfSecrets.isEmpty();
    }

    @SuppressWarnings("AssignmentToMethodParameter")
    @Override
    public Span apply(@SuppressWarnings("ParameterNameDiffersFromOverriddenParameter") Span span) {
        span = maskSecretsInTags(span);
        span = maskSecretsInLogFields(span);
        return span;
    }

    private void incrementCounter(String serviceName, String finderName, String application) {
        final FinderNameAndServiceName finderNameAndServiceName = CACHED_FINDER_NAME_AND_SECRET_NAME_OBJECTS
                .computeIfAbsent(finderName, (v -> new HashMap<>()))
                .computeIfAbsent(serviceName, (v -> new FinderNameAndServiceName(finderName, serviceName)));
        COUNTERS.computeIfAbsent(
                finderNameAndServiceName, (counter -> factory.createCounter(finderNameAndServiceName, application)))
                .increment();
    }

    public static class Factory {
        private final MetricObjects metricObjects;

        public Factory() {
            this(new MetricObjects());
        }

        public Factory(MetricObjects metricObjects) {
            this.metricObjects = metricObjects;
        }

        Counter createCounter(FinderNameAndServiceName finderAndServiceName, String application) {
            return metricObjects.createAndRegisterResettingCounter(ERRORS_METRIC_GROUP, application,
                    finderAndServiceName.getFinderName(), finderAndServiceName.getServiceName(), COUNTER_NAME);
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy