/**
 * Find Security Bugs
 * Copyright (c) Philippe Arteau, All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 3.0 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library.
 */
package com.h3xstream.findsecbugs.csrf;

import edu.umd.cs.findbugs.BugInstance;
import edu.umd.cs.findbugs.BugReporter;
import edu.umd.cs.findbugs.Detector;
import edu.umd.cs.findbugs.Priorities;
import edu.umd.cs.findbugs.ba.ClassContext;
import org.apache.bcel.classfile.AnnotationEntry;
import org.apache.bcel.classfile.ArrayElementValue;
import org.apache.bcel.classfile.ElementValue;
import org.apache.bcel.classfile.ElementValuePair;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;

import java.util.Arrays;
import java.util.List;

/**
 * Detects Spring CSRF unrestricted RequestMapping
 *
 * @author Pablo Tamarit
 */
public class SpringCsrfUnrestrictedRequestMappingDetector implements Detector {

    private static final String SPRING_CSRF_UNRESTRICTED_REQUEST_MAPPING_TYPE = "SPRING_CSRF_UNRESTRICTED_REQUEST_MAPPING";

    private static final String REQUEST_MAPPING_ANNOTATION_TYPE = "Lorg/springframework/web/bind/annotation/RequestMapping;";
    private static final String METHOD_ANNOTATION_ATTRIBUTE_KEY = "method";
    private static final List<String> UNPROTECTED_HTTP_REQUEST_METHODS = Arrays.asList("GET", "HEAD", "TRACE", "OPTIONS");

    private BugReporter bugReporter;

    public SpringCsrfUnrestrictedRequestMappingDetector(BugReporter bugReporter) {
        this.bugReporter = bugReporter;
    }

    @Override
    public void visitClassContext(ClassContext classContext) {
        JavaClass javaClass = classContext.getJavaClass();

        for (Method method : javaClass.getMethods()) {
            if (isVulnerable(method)) {
                bugReporter.reportBug(new BugInstance(this, SPRING_CSRF_UNRESTRICTED_REQUEST_MAPPING_TYPE, Priorities.HIGH_PRIORITY) //
                        .addClassAndMethod(javaClass, method));
            }
        }
    }

    @Override
    public void report() {

    }

    private static boolean isVulnerable(Method method) {

        // If the method is not annotated with `@RequestMapping`, there is no vulnerability.
        AnnotationEntry requestMappingAnnotation = findRequestMappingAnnotation(method);
        if (requestMappingAnnotation == null) {
            return false;
        }

        // If the `@RequestMapping` annotation is used without the `method` annotation attribute,
        // there is a vulnerability.
        ElementValuePair methodAnnotationAttribute = findMethodAnnotationAttribute(requestMappingAnnotation);
        if (methodAnnotationAttribute == null) {
            return true;
        }

        // If the `@RequestMapping` annotation is used with the `method` annotation attribute equal to `{}`,
        // there is a vulnerability.
        ElementValue methodAnnotationAttributeValue = methodAnnotationAttribute.getValue();
        if (isEmptyArray(methodAnnotationAttributeValue)) {
            return true;
        }

        // If the `@RequestMapping` annotation is used with the `method` annotation attribute but contains a mix of
        // unprotected and protected HTTP request methods, there is a vulnerability.
        return isMixOfUnprotectedAndProtectedHttpRequestMethods(methodAnnotationAttributeValue);
    }

    private static AnnotationEntry findRequestMappingAnnotation(Method method) {
        for (AnnotationEntry annotationEntry : method.getAnnotationEntries()) {
            if (REQUEST_MAPPING_ANNOTATION_TYPE.equals(annotationEntry.getAnnotationType())) {
                return annotationEntry;
            }
        }
        return null;
    }

    private static ElementValuePair findMethodAnnotationAttribute(AnnotationEntry requestMappingAnnotation) {
        for (ElementValuePair elementValuePair : requestMappingAnnotation.getElementValuePairs()) {
            if (METHOD_ANNOTATION_ATTRIBUTE_KEY.equals(elementValuePair.getNameString())) {
                return elementValuePair;
            }
        }
        return null;
    }

    private static boolean isEmptyArray(ElementValue methodAnnotationAttributeValue) {
        if (!(methodAnnotationAttributeValue instanceof ArrayElementValue)) {
            return false;
        }
        ArrayElementValue arrayElementValue = (ArrayElementValue) methodAnnotationAttributeValue;

        return arrayElementValue.getElementValuesArraySize() == 0;
    }

    private static boolean isMixOfUnprotectedAndProtectedHttpRequestMethods(ElementValue methodAnnotationAttributeValue) {
        if (!(methodAnnotationAttributeValue instanceof ArrayElementValue)) {
            return false;
        }
        ArrayElementValue arrayElementValue = (ArrayElementValue) methodAnnotationAttributeValue;

        // There cannot be a mix if there is no more than one element.
        if (arrayElementValue.getElementValuesArraySize() <= 1) {
            return false;
        }

        // Return `true` as soon as we find at least one unprotected and at least one protected HTTP request method.
        boolean atLeastOneUnprotected = false;
        boolean atLeastOneProtected = false;
        ElementValue[] elementValues = arrayElementValue.getElementValuesArray();
        for (ElementValue elementValue : elementValues) {
            if (UNPROTECTED_HTTP_REQUEST_METHODS.contains(elementValue.stringifyValue())) {
                atLeastOneUnprotected = true;
            } else {
                atLeastOneProtected = true;
            }
            if (atLeastOneUnprotected && atLeastOneProtected) {
                return true;
            }
        }
        return false;
    }
}